在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
编译自http://golang.org/doc/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中的放在命令后让命令后台运行的 &)。
go list.Sort() // 并行运行list.Sort,不等待其结束。
匿名函数在goroutine调用中也很有用。
func Announce(message string, delay int64) {
go func() { time.Sleep(delay) fmt.Println(message) }() //注意此处的括号,必须调用该函数。 } 在Go语言中,匿名函数是闭包(closure),其实现确保函数所引用的变量生存期与函数的生存期一样长。 这个例子不太实际,因为函数没有在运行结束时发出信号的方式。所以我们需要信道(channel)出场。
ci := make(chan int) // 无缓冲整数信道
cj := make(chan int, 0) // 无缓冲整数信道 cs := make(chan *os.File, 100) // 缓冲的文件指针信道 信道将通讯(值的交换)与同步结合在一起,确保两个计算过程(goroutine)都处于已知状态。 以前面那个后台并行排序为例。信道可用来让正在运行的goroutine等待排序完成。
c := make(chan int) // Allocate a channel.
// 在goroutine中启动排序,当排序完成时,信道上发出信号 go func() { list.Sort() c <- 1 // 发送一个信号,值是多少无所谓。 }() doSomethingForAWhile() <-c // 等待排序完成,丢弃被发送的值。
收信者(receivers)在收到数据前会一直被阻滞。如果信道是非缓冲的,则发信者(sender)在收信者接收到数据前也一直被阻滞。如果信道有缓冲区,发信者只有在数据被填入缓冲区前才被阻滞;如果缓冲区是满的,意味着发送者要等到某个收信者取走一个值。 缓冲的信道可以象信号灯一样使用,比如用来限制吞吐量。在下面的例子中,进入的请求被传递给handle,handle发送一个值到信道,接着处理请求,最后从信道接收一个值。信道缓冲区的大小限制了并发调用process的数目。
var sem = make(chan int, MaxOutstanding)
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后,它处于阻滞状态,直到接收到退出信号:
func handle(queue chan *Request) {
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是一个理想化的处理请求的函数,但是我们没有定义它所能处理的请求的具体类型。如果该类型包括了一个信道,每个客户端就可以提供自己方式进行应答
type Request struct {
args []int f func([]int) int resultChan chan int }
客户端提供一个函数、该函数的参数以及一个请求对象用来接收应答的信道
func sum(a []int) (s 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) 在服务器端,处理请求的函数是
func handle(queue chan *Request) {
for req := range queue { req.resultChan <- req.f(req.args) } } 显然要使这个例子更为实际还有很多工作要做,但这是针对速度限制、并行、非阻滞RPC系统的框架,而且其中也看不到互斥(mutex)的使用。
5. 并行(Parallelization) 这些思想的另一个应用是利用多核CPU进行并行计算。如果计算过程可以被分为多个片段,则它可以通过这样一种方式被并行化:在每个片段完成后通过信道发送信号。 假设我们有一个耗时的向量操作,而且对每个数据项的操作后的值是独立的,如下面这个理想的例子所示:
type Vector []float64
// 应用操作到 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后,我们只需要从信道中提取所有的完成信号即可。
const NCPU = 4 // CPU核数
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)。 (作者:玛瑙河。尊重他人劳动成果,转载请注明作者或出处) |
请发表评论