Golang
的一大特色就是其简单高效的 天然并发机制。
channel
是 Golang
语言中的一个 核心数据类型, 可以把他看成通道
(管道
), 所以他非常重要。主要用来解决go程的同步问题以及协程之间数据共享(数据传递)的问题。并发核心单元
通过它就可以发送或者接收数据进行通讯,这在一定程度上又进一步降低了编程的难度。
Golang
中使用 goroutine
和 channel
实现了 CSP (Communicating Sequential Processes) 模型
, channel
在 goroutine
的通信和同步中承担着重要的角色。
goroutine
运行在相同的地址空间,因此访问共享内存必须做好同步。
goroutine
奉行通过通信来共享内存,而不是共享内存来通信。
引用类型 channel
可用于多个 goroutine
通讯。其内部实现了同步,确保并发安全。
Channel 的定义与使用
和 map
类似,channel
也是一个对应 make
创建的底层数据结构的引用。
当我们复制一个channel
或用于函数参数传递时,我们只是拷贝了一个channel 引用
,因此调用者和被调用者将引用同一个channel对象
。和其它的引用类型一样,channel
的零值也是nil
。
定义一个channel
时,也需要定义发送到channel
的值的类型。channel
可以使用内置的make()
函数来创建。
我们看一段代码:
package main
import (
"fmt"
)
func main() {
ch := make(chan int) // 这里就是创建一个无缓冲的 channel
go func() {
for i := 0; i <= 6; i++ {
ch <- i // 循环写入管道数据
fmt.Println("写入管道数据", i)
}
}()
for j := 0; j <= 6; j++ {
value := <- ch // 循环读出管道数据
fmt.Println("读出管道数据", value)
}
}
我们在代码中 先创建了一个匿名函数的子go程
,和main的主go程
一起争夺 CPU
,但是我们在里面创建了一个管道,无缓冲管道
有一个规则那就是必须读写同时操作才会有效果,如果只进行读或者只进行写那么会被阻塞
,被暂时停顿等待另外一方的操作,在这里我们定义了一个容量为0的通道,这就是无缓冲通道
。
我们再看下面的代码加深 无缓冲通道
的印象
var c = make(chan int)
go func() {
// 等待 3秒后循环写入值到通道
time.Sleep(3 * time.Second)
for i := 0; i < 5; i++ {
c <- i
}
}()
select {
case value := <-c:
fmt.Println(value)
}
fmt.Println("到此为止")
// 输出结果: select 阻塞了 channel 通道, 将会先等待 3秒后打印 0, 然后打印 '到此为止'
var c = make(chan int)
go func() {
// 等待 3秒后循环写入值到通道
time.Sleep(3 * time.Second)
for i := 0; i < 5; i++ {
c <- i
}
}()
select {
case value := <-c:
fmt.Println(value)
default:
fmt.Println("我不等了")
}
fmt.Println("到此为止")
// 输出结果: select 阻塞了 channel 通道, 但是有带 default, 将会直接打印 '我不等了', 然后打印 '到此为止'
var c = make(chan int)
go func() {
// 等待 3秒后循环写入值到通道
time.Sleep(3 * time.Second)
for i := 0; i < 5; i++ {
c <- i
}
}()
fmt.Println("等待中")
<-c
fmt.Println("到此为止")
// 输出结果: 先打印 '等待中', 等待 3秒后打印 '到此为止'
var c = make(chan int)
go func() {
// 等待 3秒后循环写入值到通道
time.Sleep(3 * time.Second)
for i := 0; i < 5; i++ {
c <- i
}
}()
fmt.Println("等待中")
value := <-c
fmt.Println(value)
fmt.Println("到此为止")
// 输出结果: 先打印 '等待中', 等待 3秒后打印 0, 然后打印 '到此为止'
使用无缓冲的通道在 Goroutine 之间同步数据 (一次只能传输一个数据)
总结一下就是无缓冲特性:
- 同一时刻,同时有 读、写两端把持 channel
- 如果只有读端,没有写端,那么 “读端”阻塞
- 如果只有写端,没有读端,那么 “写端”阻塞
- 读channel: <- channel
- 写channel: channel <- 数据
使用有缓冲的通道在 Goroutine 之间同步数据 (读写数据可以同时进行)
我来描述下上面的步骤:
第一步 | 右侧的
goroutine
正在从通道接收一个值。
第二步 | 右侧的这个goroutine
独立完成了接收值的动作,而左侧的goroutine
正在发送一个新值到通道里。
第三步 | 左侧的goroutine
还在向通道发送新值,而右侧的goroutine
正在从通道接收另外一个值。这个步骤里的两个操作既不是同步的,也不会互相阻塞。
第四步 | 所有的发送和接收都完成,而通道里还有几个值,也有一些空间可以存更多的值。
有缓冲通道
就是图中所示,一方可以写入很多数据,不用等对方的操作,而另外一方也可以直接拿出数据,不需要等对方写,但是注意一点(如果写入的一方把channel
写满了,那么如果要继续写就要等对方取数据后才能继续写入,这也是一种阻塞
,读出数据也是一样,如果里面没有数据则不能取,就要等对方写入)。
有缓冲channel的定义很简单:
ch := make(chan int, 10) // chan int 只能写入读入int类型的数据, 10代表容量。
总结一下就是有缓冲特性:
- 写数据时, 直到缓冲区被填满后,“写端”才会阻塞
- 读数据时, 缓冲区被读空,“读端”才会阻塞
- len: 代表缓冲区中,剩余元素个数
- cap: 代表缓冲区的容量
最后, 举个简单的例子再次帮助理解 channel
:
同步通信 (无缓冲channel): 数据发送端,和数据接收端,必须同时在线。
比如打电话, 只有等对方接收才会通,要不然只能阻塞。
异步通信 (有缓冲channel): 数据发送端 发送完数据后立即返回。数据接收端 有可能立即读取,也可能延迟处理。
不用等对方接受, 只需发送过去就行, 比如发短信, 发邮件...
评论一下?