【Golang】并发之channel

channel

通道其实在swoole中也说过了。其实两者是差不多的,毕竟swoole是模仿golang写的一个扩展。

上一篇中说到了协程,但是单纯为了协程而协程的情况很少,大部分情况下协程内需要和协程外或者其他协程交互的。

在swoole中有提过,协程之间互相交互可以使用以下3种方式:

  1. 共享内存
  2. 使用系统消息队列
  3. 使用通道

这里最不推荐的就是使用共享内存了,因为多协程使用共享内存进行数据交互很容易出现内存污染的问题。消息队列本篇也直接跳过,这里我们单看通道channel。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

声明一个channel

通道的声明使用 var 变量 chan 元素类型。是需要声明通道中的数据类型的。

这里需要注意的是,为了节约内存,通道中的字符串,结构体之类可能会无限大的数据类型最好往通道中传入指针而不是数据实体。

实例化一个channel

channel 是一个指针类的变量,需要使用 make() 函数来进行初始化,还记得 make() 函数的作用吗?就是用来初始化 slicemapchannel 三种类型的。

实例化方法:make(chan 元素类型, [缓冲大小])

var c chan int

func main() {
	fmt.Println(c) // nil
	c = make(chan int)
	fmt.Println(c) // 0xc00003e060
}

在未初始化之前,channel的值是一个空指针nil

channel的使用

  1. 使用 chan <- 数据 来向一个通道中发送数据

  2. 使用 <- chan 来从一个同道中人取出数据

  3. 使用 close() 方法来关闭通道

var c chan int

func main() {
	c = make(chan int, 5)
	c <- 5
	fmt.Println(<-c) // 5
	close(c)
}

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

  1. 对一个关闭的通道再发送值就会导致panic。
  2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  4. 关闭一个已经关闭的通道会导致panic。

通道的缓冲区

在实例化通道的时候可以设置通道的缓冲区:make(chan 元素类型, [缓冲大小])。缓冲区的作用就是标识这个通道可以暂存的数据数量。

  1. 在缓冲区已满的时候,向通道发送数据会导致程序堵塞。
  2. 在缓冲区已空的时候,从通道获取数据也会导致程序阻塞。

当然,你也可以创建一个无缓冲区的通道 make(chan 元素类型)。对于一个无缓冲区的通道,读取和发送都是阻塞的,也叫同步通道。

遍历通道内的值

使用 range 可以遍历通道

func main() {
	c = make(chan int, 5)
	c <- 5
	c <- 5
	c <- 5
	wg.Add(1)
	go readChan(c)
	close(c)
	wg.Wait()
}

func readChan(c chan int) {
	defer wg.Done()
	for i := range c {
		fmt.Println(i)
	}
}

使用for range遍历通道,当通道被关闭的时候就会退出for range。

单向通道

有的时候,我们不希望暴露在外的通道同时拥有可读和可写的权限。这个时候就可以使用单向通道。

通道本身是可读写的,但是使用单向通道的时候可以只暴露读权限或者只暴露写权限

  1. 只读通道:func (<- chan int)
  2. 只写通道:func (chan <- int)
var c chan int
var wg sync.WaitGroup

func main() {
	c = make(chan int, 5)
	wg.Add(1)
	go writeOnly(c, 10)
	wg.Add(1)
	go readOnly(c)
	wg.Wait()
}

func writeOnly(c chan<- int, n int) {
	defer wg.Done()
	for i := 0; i < n; i++ {
		c <- i
	}
	close(c)
}

func readOnly(c <-chan int) {
	defer wg.Done()
	for i := range c {
		fmt.Println(i)
	}
}

select

select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:

select{
    case <-ch1:
        ...
    case data := <-ch2:
        ...
    case ch3<-data:
        ...
    default:
        默认操作
}

举个例子

func main() {
	ch := make(chan int, 5)
	for i := 0; i < 10; i++ {
		fmt.Println("i=", i)
		select {
		case x := <-ch:
			fmt.Println("从通道中取出", x)
		case ch <- i:
			fmt.Println("将", i, "放入通道")
		}
	}
}

输出

i= 0
将 0 放入通道
i= 1
将 1 放入通道
i= 2
将 2 放入通道
i= 3
从通道中取出 0
i= 4
从通道中取出 1
i= 5
将 5 放入通道
i= 6
将 6 放入通道
i= 7
将 7 放入通道
i= 8
从通道中取出 2
i= 9
从通道中取出 5

使用select语句能提高代码的可读性。

  1. 可处理一个或多个channel的发送/接收操作。
  2. 如果多个case同时满足,select会随机选择一个。
  3. 对于没有case的select{}会一直等待,可用于阻塞main函数。

小结

协程相关思维导图下载(使用xmind打开):golang-goroutine.xmind

程序幼儿员-龚学鹏
请先登录后发表评论
  • latest comments
  • 总共0条评论