Channel

信道

Channel通信是在Goroutine之间进行同步的主要方法;信道与映射一样,也需要通过make来分配内存。其结果值充当了对底层数据结构的引用。若提供了一个可选的整数形参,它就会为该信道设置缓冲区大小。默认值是零,表示不带缓冲的或同步的信道。

i := make(chan int)            // 整数类型的无缓冲信道
cj := make(chan int, 0)         // 整数类型的无缓冲信道
cs := make(chan *os.File, 100)  // 指向文件指针的带缓冲信道

无缓冲信道在通信时会同步交换数据,它能确保(两个协程的)计算处于确定状态。接收者在收到数据前会一直阻塞。若信道是不带缓冲的,那么在接收者收到值前,发送者会一直阻塞;若信道是带缓冲的,则发送者仅在值被复制到缓冲区前阻塞;若缓冲区已满,发送者会一直等待直到某个接收者取出一个值为止。若在关闭Channel后继续从中接收数据,接收者就会收到该Channel返回的零值。

无缓冲信道

在无缓存的Channel上的每一次发送操作都有与其对应的接收操作相配对,发送和接收操作通常发生在不同的Goroutine上(在同一个Goroutine上执行2个操作很容易导致死锁。无缓存的Channel上的发送操作总在对应的接收操作完成前发生。

var done = make(chan bool)
var msg string

func aGoroutine() {
	msg = "你好, 世界"
	done <- true
}

func main() {
	go aGoroutine()
	<-done
	println(msg)
}

若在关闭Channel后继续从中接收数据,接收者就会收到该Channel返回的零值。因此在这个例子中,用close(c)关闭管道代替 done <- false 依然能保证该程序产生相同的行为。

var done = make(chan bool)
var msg string

func aGoroutine() {
	msg = "你好, 世界"
	close(done)
}

func main() {
	go aGoroutine()
	<-done
	println(msg)
}

对于从无缓冲Channel进行的接收,发生在对该Channel进行的发送完成之前。

var done = make(chan bool)
var msg string

func aGoroutine() {
	msg = "hello, world"
	<-done
}
func main() {
	go aGoroutine()
	done <- true
	println(msg)
}

缓冲信道

带缓冲的信道可被用作信号量,例如限制吞吐量。在此例中,进入的请求会被传递给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 结束。
	}
}

对于带缓冲的Channel,对于Channel的第K个接收完成操作发生在第K+C个发送操作完成之前,其中CChannel的缓存大小。如果将C设置为0自然就对应无缓存的Channel,也即使第K个接收完成在第K个发送完成之前。因为无缓存的Channel只能同步发1个,也就简化为前面无缓存Channel的规则:对于从无缓冲Channel进行的接收,发生在对该Channel进行的发送完成之前。我们可以根据控制Channel的缓存大小来控制并发执行的Goroutine的最大数目,例如:

var limit = make(chan int, 3)

func main() {
	for _, w := range work {
		go func() {
			limit <- 1
			w()
			<-limit
		}()
	}
	select{}
}

最后一句 select{} 是一个空的管道选择语句,该语句会导致 main 线程阻塞,从而避免程序过早退出。还有for{}<-make(chan int) 等诸多方法可以达到类似的效果。因为 main 线程被阻塞了,如果需要程序正常退出的话可以通过调用os.Exit(0)实现。

内建信道

// Ctrl+C 退出
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
fmt.Printf("quit (%v)\n", <-sig)

Links

下一页