Go语言最吸引人的地方是它内建的并发支持。
在并发编程中,对共享资源的正确访问需要精确地控制,在目前的绝大多数语言中,都是通过加锁等线程同步方案来解决这一困难问题,而Go语言却另辟蹊径,它将共享的值通过通道传递(实际上多个独立执行的线程很少主动共享资源)。在任意给定的时刻,最好只有一个Goroutine能够拥有该资源。数据竞争从设计层面上就被杜绝了。为了提倡这种思考方式,Go语言将其并发编程哲学化为一句口号:"不要通过共享内存来通信,而应通过通信来共享内存。"
控制同步的方式
1.通过锁控制同步通讯
golang
func main() {
var mu sync.Mutex
mu.Lock()
go func(){
fmt.Println("你好, 世界")
mu.Unlock()
}()
mu.Lock()
}
在main()函数所在线程中执行两次mu.Lock(),当第二次加锁时会因为锁已经被占用(不是递归锁)而阻塞,main()函数的阻塞状态驱动后台线程继续向前执行。当后台线程执行到mu.Unlock()时解锁,此时打印工作已经完成了,解锁会导致main()函数中的第二个mu.Lock()阻塞状态取消,此时后台线程和主线程再没有其他的同步事件参考,它们退出的事件将是并发的:在main()函数退出导致程序退出时,后台线程可能已经退出了,也可能没有退出。虽然无法确定两个线程退出的时间,但是打印工作是可以正确完成的。
2.通过无缓存通道实现同步
golang
func main() {
done := make(chan int)
go func(){
fmt.Println("你好, 世界")
<-done
}()
done <- 1
}
根据Go语言内存模型规范,对于从无缓存通道进行的接收,发生在对该通道进行的发送完成之前。因此,后台线程<-done接收操作完成之后,main线程的done <- 1发送操作才可能完成(从而退出main、退出程序),而此时打印工作已经完成了。
上面的代码虽然可以正确同步,但是对通道的缓存大小太敏感:如果通道有缓存,就无法保证main()函数退出之前后台线程能正常打印了。更好的做法是将通道的发送和接收方向调换一下,这样可以避免同步事件受通道缓存大小的影响:
golang
func main() {
done := make(chan int, 1) // 带缓存通道
go func(){
fmt.Println("你好, 世界")
done <- 1
}()
<-done
}
3.通过waitgroup控制同步
golang
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()是等待全部的事件完成。
并发退出
Go语言中不同Goroutine之间主要依靠通道进行通信和同步。要同时处理多个通道的发送或接收操作,需要使用select关键字(这个关键字和网络编程中的select()函数的行为类似)。当select()有多个分支时,会随机选择一个可用的通道分支,如果没有可用的通道分支,则选择default分支,否则会一直保持阻塞状态。
通过select()实现超时退出:
golang
select {
case v := <-in:
fmt.Println(v)
case <-time.After(time.Second):
return // 超时
}
通过select和default分支可以很容易实现一个Goroutine的退出控制:
golang
func worker(cannel chan bool) {
for {
select {
default:
fmt.Println("hello")
// 正常工作
case <-cannel:
// 退出
}
}
}
func main() {
cannel := make(chan bool)
go worker(cannel)
time.Sleep(time.Second)
cannel <- true
}
但是通道的发送操作和接收操作是一一对应的,如果要停止多个Goroutine,那么可能需要创建同样数量的通道,这个代价太大了。其实我们可以通过close()关闭一个通道来实现广播的效果,所有从关闭通道接收的操作均会收到一个零值和一个可选的失败标志。
golang
func worker(cannel chan bool) {
for {
select {
default:
fmt.Println("hello")
// 正常工作
case <-cannel:
// 退出
}
}
}
func main() {
cancel := make(chan bool)
for i := 0; i < 10; i++ {
go worker(cancel)
}
time.Sleep(time.Second)
close(cancel)
}
我们通过close()来关闭cancel通道,向多个Goroutine广播退出的指令。不过这个程序依然不够稳健:当每个Goroutine收到退出指令退出时一般会进行一定的清理工作,但是退出的清理工作并不能保证被完成,因为main线程并没有等待各个工作Goroutine退出工作完成的机制。我们可以结合sync.WaitGroup来改进:
golang
func worker(wg *sync.WaitGroup, cannel chan bool) {
defer wg.Done()
for {
select {
default:
fmt.Println("hello")
case <-cannel:
return
}
}
}
func main() {
cancel := make(chan bool)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go worker(&wg, cancel)
}
time.Sleep(time.Second)
close(cancel)
wg.Wait()
}
用context包退出
golang
func worker(ctx context.Context, wg *sync.WaitGroup) error {
defer wg.Done()
for {
select {
default:
fmt.Println("hello")
case <-ctx.Done():
return ctx.Err()
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go worker(ctx, &wg)
}
time.Sleep(time.Second)
cancel()
wg.Wait()
}