Go语言高并发 | 青训营笔记

65 阅读3分钟

1. 语言进阶 —— 从并发编程的视角了解 go 语言的本质

1.1 Goroutine——协程

  • 协程:用户态,轻量级线程,栈KB级别
  • 线程:内核态,线程跑多个协程,栈MB级别,比较昂贵的系统资源

(课件上栈级别的内容可能有误,老师讲课时说的和根据“线程跑多个协程”可以推断)

go语言可以创建上万个协程。

  • go语言中协程的调用:只需在函数调用的时候在函数前面加上一个 go 关键字来运行程序,就能够启动一个协程。

快速打印Goroutine案例,开启多个协程打印:

package main  
  
import (  
"fmt"  
"time"  
)  
  
func hello(i int) {  
println("hello goroutine:" + fmt.Sprint(i))  
}  
  
func HelloGoRoutine() {  
for i := 0; i < 5; i++ {  
go func(j int) { //调用时加上go关键字开启协程  
hello(j)  
}(i)  
}  
time.Sleep(time.Second) //阻塞,保证子协程退出之前主协程不会退出  
}  
/*  
hello goroutine:4  
hello goroutine:0  
hello goroutine:1  
hello goroutine:2  
hello goroutine:3  
*/  

1.2 CSP (Communicating Sequential Process)

协程之间的通信。

  • go提倡通过通信共享内存而不是通过共享内存实现通信
  • 通道Channel:可以让一个Goroutine发送特定值到另一个Goroutine的通信机制,实现上述的基础
  • 临界区:通过共享内存实现数据交换。需要互斥量对共享内存加锁,影响程序性能

1.3 Channel的具体操作

channel是一个引用类型,创建方法:make(chan 元素类型, [缓冲大小]),分是否有缓冲通道,分以下两种具体示例:

  • 无缓冲通道:make(chan int)
  • 有缓冲通道:make(chan int, 2)

无缓冲通道进行通信时,导致发送的 goroutine 和接收的 goroutine 同步化,因此也称为同步通道;解决同步问题的方式就是使用带有缓冲区的有缓冲通道,缓冲大小表示存储的容量。

channel使用的例子:

A子协程发送0~9数字  
B子协程计算输入数字的平方  
主协程输出最后的平方数  
package main  
  
func CalSquare(){  
src := make(chan int) // 无缓冲的通道  
  
// 带缓冲的通道,考虑到消费者的消费速度可能会比生产者慢  
// 带缓冲的channel可以解决生产和消费不同步带来的执行效率额问题  
dest := make(chan int, 3)  
go func() { // A子协程  
defer close(src)  
for i := 0; i<10; i++ {  
src <- i  
}  
}()  
go func() { // B子协程  
defer close(dest)  
//通过 range 遍历src的数据,实现不同 channel 的通信  
for i:= range src{  
dest <- i*i  
}  
}()  
for i:= range dest{ //主协程  
//可能是一些复杂操作,用打印代替  
println(i)  
}  
}  

1.4 并发安全 Lock

通过共享内存实现通信的情况。

案例:对变量执行 5000 次 +1 操作,由 10 个协程并发执行。最终能够看到的结果是,随着+1执行数量 和 协程数量的增加,加法的执行结果准确性差距越大。加锁的方式一直准确,而不加锁的方式错误偏差越来越大。

package main  
  
import (  
"sync"  
"time"  
)  
  
var (  
x int64  
lock sync.Mutex  
)  
  
func addWithLock() {  
//加锁的情况  
for i := 0; i < 5000; i++ {  
// 加锁获取临界区的资源  
lock.Lock()  
x += 1  
// 释放临界取区的资源  
lock.Unlock()  
}  
}  
func addWithoutLock() {  
for i := 0; i < 5000; i++ {  
x += 1  
}  
}  
  
func add() {  
x = 0  
for i := 0; i < 10; i++ {  
go addWithoutLock()  
}  
time.Sleep(time.Second)  
println("add without lock:", x)  
  
x = 0  
for i:=0; i<10; i++{  
go addWithLock()  
}  
time.Sleep(time.Second)  
println("add with lock:", x)  
}  
  

1.5 线程同步 WaitGroup

一个能用于协程阻塞的标准库,存在于sync.WaitGroup位置。其中主要的几个函数(方法)即含义如下:

  • Add(delta int) 计数器 +delta
  • Done() 计数器 -1
  • Wait() 阻塞直到计数器为0

其中,对计数器有如下的规定:开启协程 计数器+1;执行结束 计数器-1;主协程阻塞直到计数器为0。
对协程优化的案例:快速打印 hello goroutine

package main  
  
import (  
"fmt"  
"sync"  
)  
  
func hello(i int) {  
println("hello goroutine:" + fmt.Sprint(i))  
}  
  
func ManyWait() {  
var wg sync.WaitGroup  
wg.Add(5)  
for i:= 0; i<5; i++{  
go func(j int) {  
defer wg.Done()  
hello(j)  
}(i)  
}  
wg.Wait()  
}