侧边栏壁纸
  • 累计撰写 98 篇文章
  • 累计创建 20 个标签
  • 累计收到 3 条评论
Go

Golang 并发通讯机制Channel

林贤钦
2022-04-23 / 0 评论 / 1 点赞 / 267 阅读 / 2,524 字
温馨提示:
本文最后更新于 2022-04-23,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

Golang 并发通讯机制Channel

一、channel 简介

一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channel可发送数据的类型。一个可以发送int类型数据的channel一般写为 chan int。

比如,使用内置make函数,我们可以创建一个channel:

ch := make(chan int) // ch 的类型 `chan int`

两个相同类型的channel可以通过 == 运算法比较。如果两个channel引用的是相同的对象,那么比较的结果为真,channel也可以和nil比较。

和map、slice类型,chan也是通过make创建底层数据结构和引用。

一个channel有发送和接受两个主要操作,都是通讯行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接受操作的goroutine。发送和接受都可以用<-运算法,在发送语句中,<-运算法分割channel和要发送的值,在接收语句中,<-运算法写在channel对象之前。一个不使用接收结果的接收操作也是合法的。

ch <- x  // 发送语句
x = <-ch // 接受语句
<-ch     // 接收语句,不需要接受结果

Channel还支持close操作,用于关闭channel随后对基于该channel的任何发送操作都将导致panic异常

对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。

使用内置的close函数就可以关闭一个channel:

close(ch)

以最简单方式调用make函数创建的是一个无缓存的channel,但是我们也可以指定第二个整型参数,对应channel的容量。如果channel的容量大于零,那么该channel就是带缓存的channel。

ch = make(chan int)    // 非缓冲 channel
ch = make(chan int, 0) // 非缓冲 channel
ch = make(chan int, 3) // 缓冲 channel , 缓冲大小3

二、不带缓存的Channel

一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。

这个例子就很好说明了两个goroutine操作同一个无缓冲的channel,都会阻塞,当发送的值通过channel成功传输后才继续,属于同步操作。无缓存Channels有时候也被称为同步Channels

func main() {
	ch1 := make(chan int)
	go pump(ch1)
	println("接受者")
	fmt.Println(<-ch1)
	println("接受完成")
}
func pump(ch chan int) {
	println("发送者")
	time.Sleep(time.Second)
	ch <- 1
	println("发送完成")
}

三、带缓冲的Channel

带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。下面的语句创建了一个可以持有三个字符串元素的带缓存Channel。

ch = make(chan string, 3)

向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。

  • 如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。
  • 相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。

我们可以在无阻塞的情况下连续向新创建的channel发送三个值:

ch <- "A"
ch <- "B"
ch <- "C"

此刻,channel的内部缓存队列将是满的,如果有第四个发送操作将发生阻塞。

image-20220423135032472

如果我们接收一个值,

fmt.Println(<-ch) // "A"

那么channel的缓存队列将不是满的也不是空的(图8.4),因此对该channel执行的发送或接收操作都不会发生阻塞。通过这种方式,channel的缓存队列解耦了接收和发送的goroutine。

image-20220423135507473

在某些特殊情况下,程序可能需要知道channel内部缓存的容量,可以用内置的cap函数获取:

fmt.Println(cap(ch)) // "3"

同样,对于内置的len函数,如果传入的是channel,那么将返回channel内部缓存队列中有效元素的个数。因为在并发程序中该信息会随着接收操作而失效,但是它对某些故障诊断和性能优化会有帮助。

fmt.Println(len(ch)) // "2"

在继续执行两次接收操作后channel内部的缓存队列将又成为空的,如果有第四个接收操作将发生阻塞:

fmt.Println(<-ch) // "B"
fmt.Println(<-ch) // "C"

四、Channel读写特性

  • 给一个 nil channel 发送数据,造成永远阻塞
  • 从一个 nil channel 接收数据,造成永远阻塞
  • 给一个已经关闭的 channel 发送数据,引起 panic
  • 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值
  • 无缓冲的channel是同步的,而有缓冲的channel是非同步的

以上5个特性是死东西,也可以通过口诀来记忆:“空读写阻塞,写关闭异常,读关闭空零”。

1

评论区