Online Go tutorial(2) - goroutine / channel / select / Mutex
Go 对协程的支持使其对于并发编程有天生的优势,标准库提供了原生的协程支持( goroutine ),协程间的通信与竞争保护工具( channel 和 sync.Metux )以及类似 C/C++ 编程中的多路复用工具 select 关键字。这里根据 Online Go tutorial 做的简单记录,适合进行备忘。
goroutine
Go 中通过 go 关键字实现协程的创建,标准的使用方法为 go + 函数调用。其中,函数调用的参数等信息在当前协程中计算和获得,而具体函数的执行流程则在新的协程中进行。与一般的协程定义一致,go 中多个协程共享地址空间等进程资源,同时由 go runtime 进行调度,故而在访问共享内存时需要资源竞争保护。
go f( x, y, z ) // 在新的协程中执行函数 f,对 x, y, z 的求值在当前协程中进行
注意,在 go 中,当 main 函数执行结束时,程序即直接退出,而不会等待其他 goroutine 的执行完成,故而在编写简单的并发代码时需要注意。常用的并发模型包括通过 channel 等待其他 go 协程,通过 sync 中的 groupwait 关键字等待一组 goroutine 的完成等。
channel
Go 中的 channel 是一种可以通过 channel 运算符 <- 进行数据发送和接收操作的命名管道。每个 channel 在使用前都必须通过 make 函数进行创建,每个 channel 的类型为 chan T。在创建 channel 时可以同时指定长度参数,指定 channel 最多可以存放的数据长度。
ch := make(chan int) // 创建处理 int 类型的 channel ch := make(chan int, 100) // 创建处理 int 类型的 channel,其长度为 100
在创建完成之后,通过 channel 运算符进行数据处理。使用 channel 操作时,channel 的接收方在没有数据时以及 channel 的发送方在 channel 为满时会阻塞,这某种程度上简化了同步操作流程,如可以通过等待 channel 信息等待其他 goroutine 的结束。
ch <- v v := <- ch // 注意 channel 运算符只有一种,其方向即为数据流动方向
channel 支持发送方对 channel 的关闭以及接收方对 channel 是否关闭进行检测。对于数据发送方,可以通过 close 函数关闭 channel,表示后续不再发送数据。对于数据的接收放,可以在接收数据时通过额外的第二个参数判断 channel 是否被关闭,注意若 channel 已被关闭时,接收方获得的数据为该 channel 类型的默认零值。for 循环的 range 语法也支持对 channel 的操作,其会循环直到 channel 中的数据被完全接收。
close( ch ) // 关闭 channel v, flag := <- ch // 通过 flag 的返回值确定 channel 是否被关闭,flag 为 false 时表示已关闭 for i := range ch { // range 语句会循环 ch 中所有的数据,注意此时若 channel 没有被关闭,for 循环会保持等待... // statements }
Google I/O 中有一些关于 channel 编程的技巧,如 Google I/O 2012 - Go Cocurrency Pattern.
select
Go 中的 select 操作通过 select 和 case 语句实现,其中 case 语句由数据操作组成( 如 channel 操作 ),当其中某个数据操作可以进行时,则会执行 case 语句对应的执行体,若多个 case 语句可以执行时,select 会任选一个 case 语句进行执行。select 语句也可以加入 default 语句,当没有 case 符合执行条件时,会执行对应的 default 语句。
select { case ch1 <- x: // 可以向 ch1 中写入数据时,该 case 语句被执行( 注意此时 x 会被写入 ch1 ) // statements case x <- ch2: // 可与向 ch2 中读出数据时,该 case 语句被执行( 注意此时 x 会读取 ch2 中的值 ) // statements default: // 没有 case 符合执行情况时,default 被执行 // statements }
sync
在基础的 Go 语法层面的支持之外,Go 通过 sync 标准库提供了诸如锁,条件变量以及 wait 操作等,具体可以可以参考官方的文档 sync package。这里做一个简单的记录。
sync.Mutex
Go 中除了支持 channel 进行协程间的数据交互外,还可以通过 sync.Mutex 来提供竞争保护。标准库的 sync.Mutex 类型实现了 Lock() 与 Unlock() 接口来完成加锁和解锁操作。
var mu sync.Mutex // 定义一个锁类型变量 mu.Lock() // 加锁操作 mu.Unlock() // 解锁操作
sync.WaitGroup
Go 中也提供了如 C/C++ 中的 waitpid 原语类似的接口,用于主协程等待其他协程的结束( 注意 Go 程序在 main 函数返回后即退出,默认不会等待其他协程的运行 )。Go 提供的操作主要基于 sync 包提供的 WaitGroup 类型的方法,包括 Add/Done/Wait。WaitGroup 可用于描述了一组需要等待结束的协程,主协程通过调用 Add 方法增加需要等待的协程的数量(增加内部维护的计数器),每个协程在结束时调用 Done 方法来表示运行结束( 从而减少内部计数器的值 )。主协程通过调用 Wait 方法来等待其他协程完成( 阻塞直到内部计数器变为 0 )。
var gw sync.GroupWait gw.Add( delta int ) // delta 常设置为 1,表示有一个协程需要等待 gw.Done() // 协程结束运行,内部计数器减一 gw.Wait() // 阻塞直至内部计数器变为 0