go语言学习笔记之并发编程

编译自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  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()
<- //  等待排序完成,丢弃被发送的值。

 

  收信者(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 ++  {
    
<- //  等待一个任务完成
  }
  
//  至此全部任务均已完成.
}

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

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

转载于:https://www.cnblogs.com/agateriver/archive/2010/05/31/1741473.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值