Go笔记 - Channels

360 阅读5分钟

channel中文含义是通道,在Go程序中channel的作用也就是通道,通道是用来传输数据的。那channel是用来连通那两个地方的呢?答案是Goroutine,不同的Goroutine通过channel相互传递信息(同一Goroutine一般不用channel传递或缓存信息)。那channel不就是线程/协程同步工具了吗?是的,channel自带同步功能,如:unbuffered channel。 同时channel 也支持异步功能buffered channel。在线程之间传递信息,很容易造成数据竞争,不用担心,不同的Goroutine同时发送或者接收数据,channel都能保证数据发送完整和数据只能被一个Goroutine 完整接收。那么在使用channel时有什么需要注意的呢?需要注意的是:

  • 不要向一个已经关闭的channel发送数据,否则会造成panic;但可以向已经关闭的channel接收数据

  • nil channel发送和接收数据都会阻塞。

  • 使用channel时要从信号的角度思考,不要把channel仅仅当成数据队列

  • 需要思考阻塞的问题,程序是否能承受channel数据满,带来的发送和接收延迟。

  • 需要考虑发送/接收谁先启动/关闭的问题。如果接收端先退出(不是使用close关闭channel,而是Goroutine退出),还使用unbuffered channel,则会造成Goroutine泄露。此时使用buffer大小为1channel更加合适。

Unbuffered Channels

无缓冲的通道,即发送和接收是同时进行的,任何一端没有准备好,另一端都会处于阻塞状态。无缓冲的通道,用于Goroutine间信号同步是非常合适的,可以通过:

  • 发送数据实时通知。
  • 通过close通道达到一次通知的目的。

下面是一个使用无缓冲通道的例子,实现类似接力的效果:

package main

import (
	"fmt"
	"os"
	"time"
)

func runner(baton chan int, over chan struct{}) {
	if baton == nil || over == nil {
		fmt.Println("baton or over channel is nil, terminate the program")
		os.Exit(-1)
	}

	count := <-baton

	fmt.Printf("runner %d is running...\n", count)
	time.Sleep(time.Second)

	if count == 4 {
		fmt.Println("runner 4 reach the endpoint")
		over <- struct{}{}
	} else {
		fmt.Printf("runner %d give the baton to the runner %d\n", count, count+1)
		baton <- count + 1
	}
}

func main() {
	baton := make(chan int)
	over := make(chan struct{})

	fmt.Println("four runners on the Line")
	for i := 0; i < 4; i++ {
		go runner(baton, over)
	}

	fmt.Println("start the race")
	baton <- 1

	<-over
	fmt.Println("The race is over")
}

输出:

four runners on the Line
start the race
runner 1 is running...
runner 1 give the baton to the runner 2
runner 2 is running...
runner 2 give the baton to the runner 3
runner 3 is running...
runner 3 give the baton to the runner 4
runner 4 is running...
runner 4 reach the endpoint
The race is over

Buffered Channels

有缓冲的通道,给人的感觉像是Goroutine间的队列,用于缓冲数据。的确,有缓冲的通道可以用于这个目的,但作为数据缓冲通道使用时需要注意:

  • 清楚缓冲通道是有大小限制的。即,通道满后,发送端无法在向通道发送数据,进入阻塞状态。所以在使用通道时,能够考虑到阻塞带来的后果。
  • 缓冲通道的大小不易过大,因为会在make时就一次分配好内存,可能会占用大量内存。

数据通道比较适用的场景有:

  • 生产者消费者模型。此时的结果更注重于消费者生产者处于阻塞状态并不会造成性能上的损失。如:从多个文件中搜索关键字的功能,此时通道中存放的是文件的路径,程序的效率完成体现在消费者的执行速率上,而生产者在通道满时处于阻塞状态并不会带来性能上的下降。
  • 一开始能确定数据通道大小的场景。那么发送端就不会处于阻塞状态,效率取决与接收端。

有缓冲的通道也可以作为信号通道使用,不过作为信号通道使用时需要在一开始就能确定通道大小。接收端通过获取一个数据作为信号,执行开始或关闭操作。下面看一个简单的例子:

package main

import (
	"time"
)

func main() {
	const count = 5
	sch := make(chan struct{}, count)
	ech := make(chan struct{}, count)

	for i := 0; i < count; i++ {
		go func(sch, ech chan struct{}) {
			<-sch
			time.Sleep(time.Second)
			ech <- struct{}{}
		}(sch, ech)
	}

	for i := 0; i < count; i++ {
		sch <- struct{}{}
	}

	index := count
	for _ = range ech {
		index -= 1
		if index == 0 {
			break
		}
    }
}

上面的例子通过两个通道,sch作为开始信号通道,告诉子Goroutine开始执行。ech作为结束信号通道,告诉主Goroutine子任务结束。

One Buffered Channels

通道大小为1的缓冲通道为什么单独拿出来说呢?因为在某些情况下有其适用的场景。在接收端先于发送端退出的情况下,如果通道是无缓冲的,那么发送端就会阻塞在发送操作上,造成Goroutine泄露。下面来看一个例子吧。

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan struct{})
	var stop chan struct{} = nil

	go func(ch chan struct{}) {
		defer func() {
			if err := recover(); err != nil {
				fmt.Println("Catch panic: ", err)
			}
		}()

		func(ch chan struct{}) {
			panic("expect receiver error")
			<-ch
			fmt.Println("Receiver exit...")
		}(ch)

	}(ch)

	go func(ch chan struct{}) {
		fmt.Println("Sender before send...")
		time.Sleep(time.Second)
		ch <- struct{}{}
		fmt.Println("Sender exit...")
	}(ch)

	<-stop
}

输出:

Sender before send...
Catch panic:  expect receiver error
fatal error: all goroutines are asleep - deadlock!
...

上面造成deadlock是正常结果,这里要看的是,Reciever由于panic异常退出时,导致Sender阻塞在发送语句上,Goroutine泄露。而将通道大小改为1,则不会造成Goroutine泄露。