Skip to content

Instantly share code, notes, and snippets.

@congjf
Last active August 29, 2015 13:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save congjf/9541968 to your computer and use it in GitHub Desktop.
Save congjf/9541968 to your computer and use it in GitHub Desktop.
Effective Go in Chinese

share by Communication

并发编程是一个大的课题,这里仅仅只是用于说明Go语言。

并发编程在许多环境中为实现正确的访问共享变量时所需要的细节不同而产生困难。Go语言在共享变量的传递时鼓励另一种方法,并且,实际上,从未通过共享分隔执行的线程而起作用。仅有一个goroutine在任意给定的时间可以访问值。设计上,数据“竞争”不可能发生。为了鼓励这一方法,我们提出一个谚语:

不要使用共享内存来进行通信;而是通过通信来共享内存。

例如,索引数通过围绕一个数值变量的互斥,来被最好的完成。但是作为高层级的方法,使用channel来控制访问,很容易编写清晰、正确的程序。

关于这个模型的思考,可以考虑一种典型的运行在一个CPU上的单线程程序。它不需要同步。现在运行另一个这样的实例,它也不需要同步。现在让这两个程序通信,如果通信是同步的,仍然不需要其他的同步。例如,Unix管道很适合这种模型。虽然Go的并发方法起源于Hare的通信序列进行(CSP),它也可以被当成类型安全的,是对Unix管道的泛化

Channel

与map类似,channel也使用make来分配内存,并且结果值作为一个到一个相关的数据结构的索引而起任用。如果一个可选择的数值参数已经被提供了,make方法为channel设置缓存的尺寸。对于一个没有缓存或同步的channel来说,默认的尺寸是0。

ci := make(chan int)            // 数值的没有缓存的channel
cj := make(chan int, 0)         // 数值的没有缓存的channel
cs := make(chan *os.File, 100)  // 有缓存的指向文件的指针的channel

无缓存的channel混合通信(值交换)与同步(保证两个计算goroutine在一个已知的状态中)。

有许多很好的使用channel的方法。在前一节我们在后台启动了一个排序。一个channel能够允许启动goroutine来等待排序的完成。

c := make(chan int)  // 为一个channel分配内存
// 开启排序goroutine; 当排序完成是,向channel发送一个信号
go func() {
    list.Sort()
    c <- 1  // 向channel发送一个信号,表示操作完成,信号的值可以是任意的值。
}()
doSomethingForAWhile()
<-c   // 等待排序完成(接收完成信号),然后将channel中的值丢弃。

直到数据接收之前,接收者都是阻塞的。如果channel没有缓存,发送者在接收器接收值之前是阻塞的。如果channel已经有缓存,发送者直到值已经被复制到缓存之前,都是阻塞的。如果缓存是满的,这意味着直到某个接收器已经接收一个值之前都会处于等待状态。

一个已经有缓存的channel,可以被用于类似一个信号,以限制实例的数量。在下面的例子中,输入的请求被传给处理器,这个处理器从channel中接收一个值,处理请求,然后发送一个值给channel来交务“信号”给下一个消费者。channel缓存的容量限制信号的数量,所以在初始化期间,我们通过填充信号来填充channel。

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    <-sem          // 等待channel的激活以释放信号
    process(r)     // 一个可能耗时的处理
    sem <- 1       // 完成之后,将信号给channel
}

func init() {
    for i := 0; i < MaxOutstanding; i++ {
        sem <- 1
    }
}

func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req)  // 使用go声明,不等待处理的完成
    }
}

因为数据同步发生在从一个channel接收值时(这意味着,发送发生在接收之前;查看Go的内存模型),信号的获得必须是在channel接收时,而不能是发送时。

这个设计有一个问题:Serve为每个输入的请求创建一个新的goroutine,然而只有它们的MaxOutstanding能够在任意的时间运行。结果是,如果进入的请求过快,这个程序可能消费无限的资源。我们通过改变Serve以限制goroutine的创建,能够处理这种不足。下面是一个解决方法,但是注意它还有一个我们将在下面说明的缺陷:

func Serve(queue chan *Request) {
    for req := range queue {
        <-sem
        go func() {
            process(req) // 缺陷
            sem <- 1
        }()
    }
}

这个缺陷是在一个Go循环中,循环变量被每个遍历的变量重复使用,所以req变量被所有的goroutine共享。这不是我们想要的。我们需要确定req对于每一个goroutine是不是唯一的。下面是一种解决方案,传递req的值:

func Serve(queue chan *Request) {
    for req := range queue {
        <-sem
        go func(req *Request) {
            process(req)
            sem <- 1
        }(req)
    }
}

比较这个版本与上面看到的版本,区别在于关闭已经被声明并运行。另一个解决方案仅仅是创建一个新的带有同样名字的变量,如:

func Serve(queue chan *Request) {
    for req := range queue {
        <-sem
        req := req // 为每一个goroutine创建一个新的req的实例
        go func() {
            process(req)
            sem <- 1
        }()
    }
}

也许这么写比较奇怪:

req := req

但是,在Go中这么做是合法并理想的方法。你得到与这个变量同名的新的版本,独特但是对每个goroutine唯一的本地影子循环变量。

返回到编写Server程序的一般问题,另一个很好管理资源的方法是开启一个goroutine处理的固定数量。goroutine的数量限制同时处理的数量。Serve方法也会在将被告知结束时接收一个channel;在启动goroutine后,Serve方法阻塞从channel的接收。

func handle(queue chan *Request) {
    for r := range queue {
        process(r)
    }
}

func Serve(clientRequests chan *Request, quit chan bool) {
    // Start handlers
    for i := 0; i < MaxOutstanding; i++ {
        go handle(clientRequests)
    }
    <-quit  // Wait to be told to exit.
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment