引言
Go语言最吸引人的地方是它内建的并发支持。Go并发编程核心的CSP理论的核心概念只有一个:同步通信。
首先明确一个概念:并发不是并行。并发更关注的是程序的设计层面,并发的程序完全是可以顺序执行的,只有在真正的多喝CPU上才可能真正地同时运行。并行更关注的是程序的运行层面,并行一般是简单的大量重复,例如,GPU中对图像处理都会有大量的并行运算。
在并发编程中,对共享资源的正确访问需要精确地控制,在目前绝大多数语言中,都是通过加锁等线程同步方案来解决这一困难,而Go语言却另辟蹊径,它将共享的值通过通道传递。在任意给定的时刻,最好只有一个Goroutine能够拥有该资源。数据竞争从设计层面上就被杜绝了。为提倡这种思考方式,Go语言将其并发编程哲学化为一句口号:“不要通过共享内存来通信,而应通过通信来共享内存。”(Do not communicate by sharing memory; instead, share memory by communicating.)
并发版本的“Hello, World”
func main() {
done := make(chan int, 1) // 带缓存通道
go func(){
fmt.Println("你好,世界")
done <- 1
}()
<-done
}
对于带缓存的通道,对通道的第K个接收完成操作发生在第K+C个发送操作完成之前,其中C是通道的缓存大小。虽然通道是带缓存的,但是main线程接收完成是在后台线程发送开始但还未完成的时刻,此时打印工作也是已经完成的。
基于带缓存通道,我们可以很容易将打印线程扩展到N个。
func main() {
done := make(chan int, 10) // 带10个缓存
// 开N个后台打印线程
for i:= 0; i < cap(done); i++ {
go func(){
fmt.Println("你好,世界")
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("你好,世界")
wg.Done()
}()
}
// 等待N个后台线程完成
wg.Wait()
}
其中,wg.Add(1)用于增加等待事件的个数,必须确保在后台线程启动之前执行(如果放到后台线程之中执行则不能保证被正常执行到)。当后台线程完成打印工作之后,调用wg.Done()表示完成一个事件。main()函数的wg.Wait()是等待全部的事件完成。