引入
Golang的一个特别重要的特性就是简化了对多任务执行的操作,使用goroutine这样一个轻量级的协程来执行异步操作。当涉及到多任务的时候,不可避免地就会想到对共享资源的读写一致性问题。比如当一个goroutine在对一个map进行写入操作的时候,另一个goroutine也在做写入操作,那么如果对于写入的顺序有要求,那么就需要有一个机制来保证这种顺序。
在Golang中,对于同步的解决方案有两种
- 锁:
sync.Mutex
或者sync.RWMutex
- 管道:chan
锁
如果多个goroutine中间没有数据传输,那么可以使用锁(sync.Mutext
)来实现对共享数据操作的一致性要求。
比如下面的这个例子,使用15个协程来分别计算每个数的阶乘,让后将这个数作为key,将阶乘作为value放入到一个map中,这个map就是一个共享资源。每个协程都在向其中写入数据,假如没有加锁,那么在写入的时候,就会报错:fatal error: concurrent map writes
。
var (
lock sync.Mutex
dict = make(map[int]int, 10)
)
func Factorial(num int) {
for i := 1; i <= num; i++ {
go factorialOne(i)
}
}
func factorialOne(num int) {
res := 1
for i := 1; i <= num; i++ {
res *= i
}
lock.Lock() // 加锁
dict[num] = res
lock.Unlock() // 解锁
}
func main() {
Factorial(15)
// 主线程等待10秒
time.Sleep(10 * time.Second)
lock.Lock() // 加锁
for key, value := range dict {
fmt.Printf("%v! = %v\n", key, value)
}
lock.Unlock() // 解锁
}
管道(chan)
当多个goroutine之间需要共享数据的同时,还需要传输数据,那么管道(chan)是一个最佳的选择。
我们可以通过make函数创建一个指定类型的chan,比如下面的例子中,我们创建了一个int类型的chan
ch1 := make(chan int)
ch2 := make(chan int, 10)
我们可以大致地认为chan有两种,一种是有缓存的(buffered),一种是没有缓存的。上面例子中的ch1
是没有缓存的,而ch2
是有缓存的。我们在使用管道的时候,需要注意这两种管道的区别。以下摘自channels。
Receivers always block until there is data to receive. If the channel is unbuffered, the sender blocks until the receiver has received the value. If the channel has a buffer, the sender blocks only until the value has been copied to the buffer; if the buffer is full, this means waiting until some receiver has retrieved a value.
我简单地做了一个总结如下:
- 读总是会阻塞,直到有数据可读
- 执行顺序
- 没有缓存的(unbuffered):先读后写
- 有缓存的(buffered)
- 管道未满:先写后读
- 管道已满:先读后写
注:读就是receive,比如<-ch
,写就是send,比如ch <- 0
。
所谓的先A后B,指的是,当程序执行到B的时候,需要阻塞,直到A执行完毕。这样就能保证A和B的执行顺序。
比如下面的例子(摘自https://golang.org/ref/mem](https://golang.org/ref/mem)
var c = make(chan int)
var a string
func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}
在上面的例子中,hello, world
肯定会打印出来。因为管道c
是unbuffered,所以遵守先读后写的规则,即程序会在写(c <- 0
)的时候阻塞,直到读(<-c
)操作结束。即:main()
函数中的写操作(c <- 0
)会先于f()
函数的读操作(<-c
)。所以在执行print(a)
的时候,f()
已经执行完毕,肯定会输出hello, world
。
但是,如果上面例子中的管道是缓存的(buffered),那么hello, world
就不会肯定输出。因为这时的管道还没有满,遵守先写后读的规则。即main()
函数中的写(c <- 0
)先执行,函数f()
中的读(<-c
)后执行。即:在main()
函数中会不会有阻塞等待发生,但是在f()
中却有,所以当执行到print(a)
的时候,并不能保证a
已经被赋值。
var c = make(chan int, 2) // 创建一个有buffer的管道
var a string
func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}