编译自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)出场。

3. 信道(Channels)
  和map一样,信道是引用类型,用make 分配内存。如果调用make时提供一个可选的整数参数,则该信道就会被分配相应大小的缓冲区。缓冲区大小默认为0,对应于无缓冲信道或者同步信道。 

ci := make(chan int// 无缓冲整数信道
cj := make(chan int0// 无缓冲整数信道
cs := make(chan *os.File, 100// 缓冲的文件指针信道

  信道将通讯(值的交换)与同步结合在一起,确保两个计算过程(goroutine)都处于已知状态。

  以前面那个后台并行排序为例。信道可用来让正在运行的goroutine等待排序完成。

c := make(chan int// Allocate a channel.
// 在goroutine中启动排序,当排序完成时,信道上发出信号
go func() {
  list.Sort()
  c 
<- 1 // 发送一个信号,值是多少无所谓。
}()
doSomethingForAWhile()
<-// 等待排序完成,丢弃被发送的值。

 

  收信者(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([]
intint
  resultChan chan 
int
}

 

  客户端提供一个函数、该函数的参数以及一个请求对象用来接收应答的信道

func sum(a []int) (s int) {
for _, v := range a {
  s += v
}
return
}

request := &Request{[]int{345}, 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++ {
    
<-// 等待一个任务完成
  }
  
// 至此全部任务均已完成.
}

  Go编译器gc(6g等)的当前实现在默认情况下并不会使这段代码并行化。对于用户级别的进程,它仅使用单核。任意数目的goroutines都可以在系统调用中被阻滞,但是默认情形下任意时刻只能有一个goroutine可以执行用户级代码。如果你需要多核CPU的并行计算,必须通知运行时并行执行的goroutines数即GOMAXPROCS 。有两种方式设置GOMAXPROCS,一个就是设置环境变量GOMAXPROCS,将其设为CPU核数;另一种方式就是导入runtime包并调用runtime.GOMAXPROCS(NPCU)。

(作者:玛瑙河。尊重他人劳动成果,转载请注明作者或出处)

posted on 2010-05-31 14:57  玛瑙河  阅读(5792)  评论(0编辑  收藏  举报