go协程:管道 · go学习3部曲:入门,进阶,实战 · 看云
如果说 goroutine 是 Go语言程序的并发体的话,那么 channel(信道) 就是 它们之间的通信机制。
channel,是一个可以让一个 goroutine 与另一个 goroutine 传输信息的通道,go称之为管道(pipeline)
管道,连接多个goroutine程序 ,它是一种队列式的数据结构,遵循先入先出的规则
##管道的定义与使用
每个管道都只能传递一种数据类型的数据,所以在声明的时候,需指定管道的数据类型(string int 等等)
~~~
var 管道实例 chan 管道类型
// 定义容量为10的管道
var 管道实例 [10]chan 管道类型
~~~
声明后的信道,其零值是nil,无法直接使用,必须配合make函进行初始化。
~~~
管道实例 = make(chan 信道类型)
~~~
亦或者:
~~~
管道实例 := make(chan 管道类型)
~~~
传输int类型的管道,代码示例
~~~
pipline := make(chan int)
~~~
管道的数据操作,就两种:发送数据与读取数据
~~~
// 往管道中发送数据(写管道)
pipline<- 200
// 从管道中取出数据,并赋值给mydata
mydata := <-pipline
~~~
管道用完了,可以对其进行关闭,避免程序一直在等待。但是关闭信道后,接收方仍然可以从信道中取到数据,只是接收到的会永远是 0
~~~
close(pipline)
~~~
对一个已关闭的信道再关闭,是会报错的。所以关闭前,需判断管道是否已关闭
当从管道中读取数据时,有多个返回值,其中第二个示 信道是否被关闭,如果已经被关闭,ok 为 false,若还没被关闭,ok 为true。
~~~
x, ok := <-pipline
~~~
## 信道的容量与长度
一般创建管道使用 make 函数,make 函数接收两个参数
* 第一个参数:必填,指定信道类型
* 第二个参数:选填,不填默认为0,指定管道的**容量**(可缓存多少数据)
对于管道的容量,很重要,注意以下几点:
* 当容量为0时,说明管道中不能存放数据,在发送数据时,必须要求立马程序接收,否则会报错。此时的管道称之为**无缓冲管道**。
* 当容量为1时,说明管道只能缓存一个数据,若管道中已有一个数据,此时再往里发送数据,会造成程序阻塞。 利用这点可以利用管道来做锁。
* 当容量大于1时,管道中可以存放多个数据,可以用于多个协程之间的通信管道,共享资源。
管道相当于是一个容器。
若将它比做一个纸箱子
* 它可以装10本书,代表其容量为10
* 当前只装了1本书,代表其当前长度为1
管道的容量,可以使用 cap 函数获取 ,而管道的长度,可以使用 len 长度获取
~~~
package main
import "fmt"
func main() {
pipline := make(chan int, 10)
fmt.Printf("管道可缓冲 %d 个数据\n", cap(pipline))
pipline<- 1
fmt.Printf("管道中当前有 %d 个数据", len(pipline))
}
~~~
输出如下
~~~
管道可缓冲 10 个数据
管道中当前有 1 个数据
~~~
## 缓冲信道与无缓冲信道
按照是否可缓冲数据可分为:**缓冲信道**与**无缓冲信道**
**缓冲信道**
允许信道里存储一个或多个数据,这意味着,设置了缓冲区后,发送端和接收端可以处于异步的状态。
~~~
pipline := make(chan int, 10)
~~~
**无缓冲信道**
在信道里无法存储数据,这意味着,**接收端必须先于发送端**准备好,以确保管道发送完数据后,立马接收数据,否则发送端就会造成阻塞,原因很简单,信道中无法存储数据。也就是说发送端和接收端是同步运行的。
~~~
pipline := make(chan int)
// 或者
pipline := make(chan int, 0)
~~~
## 双向信道与单向信道
通常情况下,我们定义的管道都是双向通道,可发送数据,也可以接收数据
但有时候,我们希望对管道的数据流向做一些控制,比如这个管道只能接收数据或者这个信道只能发送数据。
因此,就有了**双向信道**和**单向信道**两种分类。
**双向信道**
默认情况下定义的信道都是双向的,比如下面代码
~~~
import (
"fmt"
"time"
)
func main() {
pipline := make(chan int)
go func() {
fmt.Println("准备发送数据: 100")
pipline <- 100
}()
go func() {
num := <-pipline
fmt.Printf("接收到的数据是: %d", num)
}()
// 主函数sleep,使得上面两个goroutine有机会执行
time.Sleep(1)
}
~~~
**单向信道**
单向信道,可以细分为**只读信道**和**只写信道**。
定义只读信道
~~~
var pipline = make(chan int)
type Receiver = <-chan int // 关键代码:定义别名类型
var receiver Receiver = pipline
~~~
定义只写信道
~~~
var pipline = make(chan int)
type Sender = chan<- int // 关键代码:定义别名类型
var sender Sender = pipline
~~~
仔细观察,区别在于`<-`符号在关键字`chan`的左边还是右边。
* `<-chan`表示这个信道,只能从里发出数据,对于程序来说就是只读
* `chan<-`表示这个信道,只能从外面接收数据,对于程序来说就是只写
有同学可能会问:为什么还要先声明一个双向信道,再定义单向通道呢?比如这样写
~~~
type Sender = chan<- int
sender := make(Sender)
~~~
代码是没问题,但是你要明白信道的意义是什么?(**以下是我个人见解**
信道本身就是为了传输数据而存在的,如果只有接收者或者只有发送者,那信道就变成了只入不出或者只出不入了吗,没什么用。所以只读信道和只写信道,唇亡齿寒,缺一不可。
当然了,若你往一个只读信道中写入数据 ,或者从一个只写信道中读取数据 ,都会出错。
完整的示例代码如下,供你参考:
~~~
import (
"fmt"
"time"
)
//定义只写信道类型
type Sender = chan<- int
//定义只读信道类型
type Receiver = <-chan int
func main() {
var pipline = make(chan int)
go func() {
var sender Sender = pipline
fmt.Println("准备发送数据: 100")
sender <- 100
}()
go func() {
var receiver Receiver = pipline
num := <-receiver
fmt.Printf("接收到的数据是: %d", num)
}()
// 主函数sleep,使得上面两个goroutine有机会执行
time.Sleep(1)
}
~~~
## 遍历通道
遍历信道,可以使用 for 搭配 range关键字,在range时,要确保管道是处于关闭状态,否则循环会阻塞。
~~~
import "fmt"
func fibonacci(mychan chan int) {
n := cap(mychan)
x, y := 1, 1
for i := 0; i < n; i++ {
mychan <- x
x, y = y, x+y
}
// 记得 close 信道
// 不然主函数中遍历完并不会结束,而是会阻塞。
close(mychan)
}
func main() {
pipline := make(chan int, 10)
go fibonacci(pipline)
for k := range pipline {
fmt.Println(k)
}
}
~~~
## 用管道来做锁
当信道里的数据量已经达到设定的容量时,此时再往里发送数据会阻塞整个程序。
利用这个特性,可以用当他来当程序的锁。
示例如下,详情可以看注释
~~~
package main
import {
"fmt"
"time"
}
// 由于 x=x+1 不是原子操作
// 所以应避免多个协程对x进行操作
// 使用容量为1的信道可以达到锁的效果
func increment(ch chan bool, x *int) {
ch <- true
*x = *x + 1
<- ch
}
func main() {
// 注意要设置容量为 1 的缓冲信道
pipline := make(chan bool, 1)
var x int
for i:=0;i<1000;i++{
go increment(pipline, &x)
}
// 确保所有的协程都已完成
// 以后会介绍一种更合适的方法(Mutex),这里暂时使用sleep
time.Sleep(3)
fmt.Println("x 的值:", x)
}
~~~
输出如下
~~~
x 的值:1000
~~~
如果不加锁,输出会小于1000。
## 几个注意事项
1. 关闭一个未初始化的 channel 会产生 panic
2. 重复关闭同一个 channel 会产生 panic
3. 向一个已关闭的 channel 发送消息会产生 panic
4. 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已被读取,则会读取到该类型的零值。
5. 从已关闭的 channel 读取消息永远不会阻塞,并且会返回一个为 false 的值,用以判断该 channel 是否已关闭(x,ok := <- ch)
6. 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息
7. channel 在 Golang 中是一等公民,它是线程安全的,面对并发问题,应首先想到 channel。