引入
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)
}