编译自effective_go.html#concurrency (翻译错误之处,敬请指正)
1. 通过通讯共享内存(Share by communicating):
Do not communicate by sharing memory; instead, share memory by communicating.
不要通过内存共享进行通讯;应当通过通讯来共享内存。使用信道(channels)来控制变量的访问可以更为容易地编写出清晰、正确的程序。
2. Goroutines:
为什么创造goroutine这个新词? 原因就是现有的术语,比如线程、协程、进程等等都不能精确的表达其所要表达的内涵(译者在这里也建议不要将其翻译成中文,因为中文里也没有任何词可以精确的表示其内涵)。一个Goroutine就是一个与其它的goroutine在同一地址空间并发执行的函数,这句话有点绕口,但说明了两个意思:一个goroutine就是一个函数;多个goroutine在同一地址空间并发执行。
Goroutine是轻量的,比直接分配栈空间的方法要耗用少得多的内存。它起始栈(stack)很小,通过按需分配(和释放)堆(heap)空间来增加内存使用。 Goroutine可被多个OS线程复用,所以如果一个goroutine被阻滞(比如等待I/O时),其它的可以继续运行。这种设计隐藏了线程创建和线程管理的复杂性。 通过在函数或方法前冠以关键词go可以在一个新的goroutine中调用该函数。当调用完成后,该goroutine退出。(效果类似于Unix Shell中的放在命令后让命令后台运行的 &)。
匿名函数在goroutine调用中也很有用。
go func() {
time.Sleep(delay)
fmt.Println(message)
}() //注意此处的括号,必须调用该函数。
}
在Go语言中,匿名函数是闭包(closure),其实现确保函数所引用的变量生存期与函数的生存期一样长。
这个例子不太实际,因为函数没有在运行结束时发出信号的方式。所以我们需要信道(channel)出场。
3. 信道(Channels)
和map一样,信道是引用类型,用make 分配内存。如果调用make时提供一个可选的整数参数,则该信道就会被分配相应大小的缓冲区。缓冲区大小默认为0,对应于无缓冲信道或者同步信道。
cj := make(chan int, 0) // 无缓冲整数信道
cs := make(chan *os.File, 100) // 缓冲的文件指针信道
信道将通讯(值的交换)与同步结合在一起,确保两个计算过程(goroutine)都处于已知状态。
以前面那个后台并行排序为例。信道可用来让正在运行的goroutine等待排序完成。
// 在goroutine中启动排序,当排序完成时,信道上发出信号
go func() {
list.Sort()
c <- 1 // 发送一个信号,值是多少无所谓。
}()
doSomethingForAWhile()
<-c // 等待排序完成,丢弃被发送的值。
收信者(receivers)在收到数据前会一直被阻滞。如果信道是非缓冲的,则发信者(sender)在收信者接收到数据前也一直被阻滞。如果信道有缓冲区,发信者只有在数据被填入缓冲区前才被阻滞;如果缓冲区是满的,意味着发送者要等到某个收信者取走一个值。
缓冲的信道可以象信号灯一样使用,比如用来限制吞吐量。在下面的例子中,进入的请求被传递给handle,handle发送一个值到信道,接着处理请求,最后从信道接收一个值。信道缓冲区的大小限制了并发调用process的数目。
func handle(r *Request) {
sem <- 1 // 等待队列缓冲区非满
process(r) // 处理请求,可能会耗费较长时间.
<-sem // 请求处理完成,准备处理下一个请求
}
func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) //不等待handle完成
}
}
通过启动固定数目的handle goroutines也可以实现同样的功能,这些goroutines都从请求信道中读取请求。Goroutines的数目限制了并发调用process的数目。Serve函数也从一个信道中接收退出信号;在启动goroutines后,它处于阻滞状态,直到接收到退出信号:
for r := range queue {
process(r)
}
}
func Serve(clientRequests chan *clientRequests, quit chan bool) {
// 启动请求处理
for i := 0; i < MaxOutstanding; i++ {
go handle(clientRequests)
}
<-quit // 等待退出信号
}
4. 通过信道传递信道(Channels of channels)
Go最重要的特性之一就是: 信道是Go最重要的特性之一就是: 信道可以像其它类型的数值一样被分配内存并传递。此特性常用于实现安全且并行的去复用(demultiplexing)。前面的例子中,handle是一个理想化的处理请求的函数,但是我们没有定义它所能处理的请求的具体类型。如果该类型包括了一个信道,每个客户端就可以提供自己方式进行应答
args []int
f func([]int) int
resultChan chan int
}
客户端提供一个函数、该函数的参数以及一个请求对象用来接收应答的信道
for _, v := range a {
s += v
}
return
}
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// 发送请求
clientRequests <- request
// 等待响应.
fmt.Printf("answer: %d\n", <-request.resultChan)
在服务器端,处理请求的函数是
for req := range queue {
req.resultChan <- req.f(req.args)
}
}
显然要使这个例子更为实际还有很多工作要做,但这是针对速度限制、并行、非阻滞RPC系统的框架,而且其中也看不到互斥(mutex)的使用。
5. 并行(Parallelization)
这些思想的另一个应用是利用多核CPU进行并行计算。如果计算过程可以被分为多个片段,则它可以通过这样一种方式被并行化:在每个片段完成后通过信道发送信号。
假设我们有一个耗时的向量操作,而且对每个数据项的操作后的值是独立的,如下面这个理想的例子所示:
// 应用操作到 v[i], v[i+1] ... v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
for ; i < n; i++ {
v[i] += u.Op(v[i])
}
c <- 1 // 发送完成信号
}
我们在一个循环中为每个CPU启动一个独立的计算片段,这些片段可以以任意的顺序执行,执行顺序在这里是无关紧要的。在启动所有的goroutines后,我们只需要从信道中提取所有的完成信号即可。
func (v Vector) DoAll(u Vector) {
c := make(chan int, NCPU) // Buffering optional but sensible.
for i := 0; i < NCPU; i++ {
go v.DoSome(i*len(v)/NCPU, (i+1)*len(v)/NCPU, u, c)
}
//从信道中取出所有信号
for i := 0; i < NCPU; i++ {
<-c // 等待一个任务完成
}
// 至此全部任务均已完成.
}
Go编译器gc(6g等)的当前实现在默认情况下并不会使这段代码并行化。对于用户级别的进程,它仅使用单核。任意数目的goroutines都可以在系统调用中被阻滞,但是默认情形下任意时刻只能有一个goroutine可以执行用户级代码。如果你需要多核CPU的并行计算,必须通知运行时并行执行的goroutines数即GOMAXPROCS 。有两种方式设置GOMAXPROCS,一个就是设置环境变量GOMAXPROCS,将其设为CPU核数;另一种方式就是导入runtime包并调用runtime.GOMAXPROCS(NPCU)。
(作者:玛瑙河。尊重他人劳动成果,转载请注明作者或出处)
有疑问加站长微信联系(非本文作者)