与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.
}