Go 通道(channel)与协程间通信
协程间通信
协程中可以使用共享变量来通信,但是很不提倡这样做,因为这种方式给所有的共享内存的多线程都带来了困难。
在 Go 中有一种特殊的类型,通道(channel),就像一个可以用于发送类型化数据的管道,由其负责协程之间的通信,从而避开所有由共享内存导致的陷阱;这种通过通道进行通信的方式保证了同步性。
数据在通道中进行传递:在任何给定时间,一个数据被设计为只有一个协程可以对其访问,所以不会发生数据竞争。数据的所有权(可以读写数据的能力)也因此被传递。
通道服务于通信的两个目的:值的交换,同步的,保证了两个计算(协程)任何时候都是可知状态。
声明与初始化
通道的声明格式如下:
var identifier chan datatype
未初始化的通道的值为 nil。
从声明的格式能够看出来,通道只能传输一种类型的数据,比如 chan int 或者 chan string,所有的类型都可以用于通道,空接口 interface{} 也可以。
通道也是引用类型,所以我们使用 make() 函数来给它分配内存。
var ch1 chan string
ch1 = make(chan string)
//或者简写为
ch2 := make(chan string)
通信操作符
操作符 <-
直观的表示了数据的传输,信息按照箭头的方向流动。
- 发送(数据流向通道)
ch <- int1
表示:将变量 int1 放入通道中,用通道 ch 发送变量 int1 - 接收(数据从通道中流出)
int2 = <- ch
表示:变量 int2 从通道 ch 接收数据(获取新值)。
下面的例子展示了两个协程之间的通信:
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go sendData(ch)
go getData(ch)
time.Sleep(1e9)
}
func sendData(ch chan string){
ch <- "golang"
}
func getData(ch chan string){
fmt.Println(<- ch)
}
输出结果:
在 main() 方法的最后一行中,使用了 time 包中的 sleep 函数来暂停1秒,以确保 main() 方法会在另个两个协程之后结束,如果不在 main() 方法中等待,协程会随着程序的结束而消亡。
通道阻塞
默认情况下,通信是同步且无缓冲的,通道的发送/接收操作在对方准备好之前都是阻塞的:
- 对于同一个通道,在没有接受者接收数据之前,发送操作会被阻塞。
- 对于同一个通道,在没有发送者发送数据之前,接收操作会被阻塞。
现在我们把上面的例子修改一下,去掉 sendData() 方法前的 go 关键字:
func main() {
ch := make(chan string)
//go sendData(ch)
sendData(ch)
go getData(ch)
time.Sleep(1e9)
}
输出结果:
运行程序后出错了,抛出了一个 panic,这是为什么呢?
这是因为 Go 程序在运行时会检查所有的协程,查找是否存在有阻塞(读取或者写入某个通道)的情况。
而上面这段代码中的 sendData() 方法阻塞了 main() 方法,导致 go getData()
无法执行,也就是说通道的接收操作也就无法被执行,而 sendData() 中的发送操作也会一直等待,此时所有的协程都休眠了,程序无法继续运行。这就是死锁(deadlock)形式。
如果我们接着再修改一下代码,保留 sendData() 方法的关键字,而去掉 getData() 方法的关键字:
func main() {
ch := make(chan string)
go sendData(ch)
getData(ch)
time.Sleep(1e9)
}
因为发送和接收操作都会被执行,所以结果是正常输出“golang”。
通信是一种同步形式:通过通道,两个协程在通信(协程会和)中某刻同步交换数据。无缓冲通道成为了多个协程同步的完美工具。
带缓冲的通道
一个无缓冲通道只能包含 1 个元素,但是我们可以为通道提供了一个缓存,来扩展通道可容纳的元素个数。
在 make() 函数中设置容量:
buf := 100
ch1 := make(chan string, buf)
- 在缓冲被满载(缓冲被全部使用)之前,给一个带缓冲的通道发送数据是不会阻塞的
- 在缓冲被清空之前,从通道读取数据也不会阻塞。
如果容量大于 0,通道就是异步的了:缓冲满载(发送)或者变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收。如果容量是 0 或者未设置,通信仅在收发双方准备好的情况下才可以成功。