GO语言实现同步的方式有很多,互斥量(sync.Mutex)、channel通信等等;
今天总结一下Go中的channel方式!
channel通信包含两种模式:一种是带无缓冲的channel,另一种是有缓冲的channel;
1、无缓冲方式
在无缓存的channel上的每一次发送操作都会有对应的接受操作相配对,发送和接受操作通常发生在两个goroutine上(在同一个goroutine上执行两个操作容易导致死锁),无缓存的channel上的发送操作总是在接受操作之前发生;
首先通过无缓冲方式来实现同步:
func main() {
done := make(chan int)
go func(){
fmt.Println("hello world!")
<-done
}()
done <- 1
}
根据GO语言内存模型规范,对于从无缓冲channel进行的接收,发生在对该channel进行的发送完成之前。因此后台线程完成 <- done 接收操作之后,main函数的 done <- 1 发送操作才有可能完成(从而退出main、退出程序),而此时打印工作已经完成。
上述代码虽然可以完成同步,但是对管道的缓冲大小太敏感了,如果管道有缓冲的话就不能够保证main退出后台线程正常打印。
更好的做法是将接收和发送方向调换一下,这样可以避免同步受缓冲大小的影响;
func main() {
done := make(chan int, 1) // 带缓存的管道
go func(){
fmt.Println("hello world!")
done <- 1
}()
<-done
}
2、有缓冲的方式
对于带缓冲的Channel,对于Channel的第K个接收完成操作发生在第K+C个发送操作完成之前,其中C是Channel的缓存大小。虽然管道是带缓存的,main线程接收完成是在后台线程发送开始但还未完成的时刻,此时打印工作也是已经完成的。
基于带缓存的管道,我们可以很容易将打印线程扩展到N个。下面的例子是开启10个后台线程分别打印:
func main() {
done := make(chan int, 10) // 带 10 个缓存
// 开N个后台打印线程
for i := 0; i < cap(done); i++ {
go func(){
fmt.Println("hello world!")
done <- 1
}()
}
// 等待N个后台线程完成
for i := 0; i < cap(done); i++ {
<-done
}
}
对于这种要等待N个线程完成后再进行下一步的同步操作有一个简单的做法,就是使用sync.WaitGroup来等待一组事件:
func main() {
var wg sync.WaitGroup
// 开N个后台打印线程
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
fmt.Println("hello world!")
wg.Done()
}()
}
// 等待N个后台线程完成
wg.Wait()
}
其中wg.Add(1)用于增加等待事件的个数,必须确保在后台线程启动之前执行(如果放到后台线程之中执行则不能保证被正常执行到)。当后台线程完成打印工作之后,调用wg.Done()表示完成一个事件。main函数的wg.Wait()是等待全部的事件完成。
个人github账号:https://github.com/SpecialAll