本节学习
- 什么是信道?
- 如何声明信道?
- 信道如何收发数据?
- 什么是死锁?
- 什么是单向信道?
- 如何关闭信道?
- 使用 for range 遍历信道
- 如何缓冲信道
- 计算信道的容量和长度
什么是信道?
信道是实现 Go 协程间的通信的桥梁,信道可以想像成 Go 协程之间通信的管道。如同管道中的水会从一端流到另一端,通过使用信道,数据也可以从一端发送,在另一端接收。
如何声明信道
所有信道都关联了一个类型。信道只能运输这种类型的数据,而运输其他类型的数据都是非法的。
chan T 表示 T 类型的信道。
信道的零值为 nil。信道的零值没有什么用,应该像对 map 和切片所做的那样,用 make 来定义信道
package main
import "fmt"
func main() {
var a chan int // 声明信道 零值
if a == nil {
fmt.Println("channel a is nil, going to define it")
a = make(chan int)
fmt.Printf("Type of a is %T", a)
}
}
信道也能简短声明
a := make(chan int)
信道如何收发数据
<- 数据操作符
信道发数据
data <- chan
信道接受数据
chan <- data
下面看一个完整的例子
package main
import "fmt"
func main() {
a := 12
b := make(chan int,1)
b <- a
c :=<- b
fmt.Println(c)
fmt.Println(b)
}
- 发送与接收默认是阻塞的。
这是什么意思?当把数据发送到信道时,程序控制会在发送数据的语句处发生阻塞,直到有其它 Go 协程从信道读取到数据,才会解除阻塞。与此类似,当读取信道的数据时,如果没有其它的协程把数据写入到这个信道,那么读取过程就会一直阻塞着。
信道的这种特性能够帮助 Go 协程之间进行高效的通信,不需要用到其他编程语言常见的显式锁或条件变量。
我们用一个例子演示一下
package main
import (
"fmt"
)
func read(num chan int){
fmt.Println(num) // 2
num <- 12
time.Sleep(100 * time.Millisecond)
fmt.Println("可能不会执行") // 4
}
func main() {
done := make(chan int)
go read(done)
fmt.Println("马上要开始等待了") // 1
b := <-done
fmt.Printf("阻塞接受了值 %d",b) // 3
}
代码执行顺序 1 - 2 - 3 - 4(已经不执行了)
注意以上日志输出顺序 1出为甚么先执行,由于go 是并发的,所以1处不会等待read执行完毕就已经开始执行了,但是 b := <-done 是阻塞接受的,当1执行完毕时,当前的协程就卡主了,当read中的num <- 12 只要执行完毕,不管后面有没有语句,b :<- done 立马就开始接受数据,此时3处就已经执行了,4处的代码就不会值了
注意如果我们 将 b := <-done 改为 <-done 是完全合法的 此操作 是从信道中取出值
下面演示一个协程的核心用法,多协程协同工作
求半径为r的圆的面积和周长
package main
import (
"math"
"fmt"
)
func calculateArea(r float64,area chan float64) {
area <- r * r * math.Pi
}
func calculateLength(r float64, length chan float64) {
length <- r * 2 * math.Pi
}
func main() {
area := make(chan float64)
length := make(chan float64)
// 多协程并发计算
go calculateArea(3.0,area)
go calculateLength(3.0,length)
// 等待计算结果
a := <- area
b := <- length
fmt.Println(a)
fmt.Println(b)
}
死锁
package main
func main() {
ch := make(chan int)
ch <- 5
}
由于没有其他协程接受数据,所以就产生了死锁
import "fmt"
func read(data *chan int){
b := <- *data
fmt.Println(b)
}
func main() {
ch := make(chan int)
go read(&ch) // 这个协程在等待接受数据
ch <- 5
}
由于 read 协程等待接受数据,所以就不会产生死锁
看下面的例子
package main
import (
"fmt"
"time"
)
func read(data *chan int,num int){
b := <- *data
fmt.Println(b)
fmt.Println(num)
}
func main() {
ch := make(chan int)
go read(&ch,1) // 这个协程在等待接受数据
go read(&ch,2) // 这个协程在等待接受数据
go read(&ch,3) // 这个协程在等待接受数据
ch <- 5
time.Sleep(1000 * time.Millisecond)
}
等待的协程会产生竞争,夺取这个数据,一旦有协程拿到这个数了,信道中的数据就会被销毁,其他协程会继续在哪里等待,知道有新的数据到信道里面
看下面的例子
package main
import (
"fmt"
"time"
)
func read(data *chan int,num int){
b := <- *data
fmt.Printf("%d-%d\n",num,b)
}
func main() {
ch := make(chan int)
go read(&ch,0) // 这个协程在等待接受数据
go read(&ch,1) // 这个协程在等待接受数据
go read(&ch,2) // 这个协程在等待接受数据
go read(&ch,3) // 这个协程在等待接受数据
go read(&ch,4) // 这个协程在等待接受数据
go read(&ch,5) // 这个协程在等待接受数据
go read(&ch,6) // 这个协程在等待接受数据
ch <- 5
ch <- 4
ch <- 3
ch <- 2
ch <- 1
ch <- 0
ch <- 10
time.Sleep(1000 * time.Millisecond)
}
注意 注意 我们向通道输送数据的顺序和其它协程接受数据的循序,发送数据肯定是顺序发送,因为在一个协程中,但是接受数据的顺序是在不同协程,所以这个我们没法控制
7 个子协程在等待数据,我们给信道输入了7次值,如果输入第八次值,会怎么样?
就会产生死锁
我们在看一个例子
package main
import (
"fmt"
"time"
)
func read(data *chan int,num int){
b := <- *data
fmt.Println(num)
fmt.Printf("%d-%d\n",num,b)
}
func main() {
ch := make(chan int)
go read(&ch,0) // 这个协程在等待接受数据
go read(&ch,1) // 这个协程在等待接受数据
go read(&ch,2) // 这个协程在等待接受数据
go read(&ch,3) // 这个协程在等待接受数据
go read(&ch,4) // 这个协程在等待接受数据
go read(&ch,5) // 这个协程在等待接受数据
go read(&ch,6) // 这个协程在等待接受数据
ch <- 6
time.Sleep(300)
ch <- 5
time.Sleep(300)
ch <- 4
time.Sleep(300)
ch <- 3
time.Sleep(300)
ch <- 2
time.Sleep(300)
ch <- 1
time.Sleep(300)
ch <- 0
time.Sleep(1000 * time.Millisecond)
}
从这张图中可以总结如下规律
- 1.读取信道数据的顺序,与协程等待的顺序没有关系
- 2.协程就算优先获取信道里的数据,但是由于go的并发性,它处理数据也可能落后于其他协程
- 3.在主协程中,只要向协程中写数据,就必须在其它协程中读,并且读的顺序一定要在写的前面,不然对于没有缓冲的信道是写不进去了,在主协程读信道的值,必须在读信道之前,有子协程方法先调用,里面有向信道里面写值的操作,不然主协程会卡主,后面的代码也没有办法执行
单向信道
我们目前讨论的信道都是双向信道,即通过信道既能发送数据,又能接收数据。其实也可以创建单向信道,这种信道只能发送或者接收数据
package main
import "fmt"
func sendData(sendch chan<- int) {
sendch <- 10
}
func main() {
sendch := make(chan<- int) // 定义一个只能写的信道
go sendData(sendch)
fmt.Println(<-sendch)
}
这里你会疑问,如果只定义一个只能写的信道有什么意义
下面我们看一个例子
package main
import "fmt"
func sendData(sendch chan<- int) {
sendch <- 10
}
func main() {
ch := make(chan int) // 定义一个只能写的信道
go sendData(ch)
a := <- ch
fmt.Println(a)
}
sendData 的参数是一个只能写的信道类型,这样就能限制sendData 方法中,不会对信道类型参数,进行读的操作
下面是一个重要的要义
不管是读通道 还是写 通道,都不能转换成双向通道,但是双向通到是可以转换成单向通道的
如何关闭信道
当从信道接收数据时,接收方可以多用一个变量来检查信道是否已经关闭
v, ok := <- ch
如果成功接收信道所发送的数据,那么 ok 等于 true。而如果 ok 等于 false,说明我们试图读取一个关闭的通道。从关闭的信道读取到的值会是该信道类型的零值。
下面看一个实例
package main
import (
"fmt"
"time"
)
func read(num <-chan bool) {
v,ok := <- num
if(ok){
fmt.Println(v)
}else{
fmt.Println("信道关闭了")
}
}
func main() {
ch := make(chan bool) // 定义一个只能写的信道
go read(ch)
close(ch)
ch <- true
time.Sleep(time.Millisecond * 1000)
}
注意 信道关闭后,就不能向信道里面输送值了,不然出抛出一个panic
for range
for range 可以循环监听信道,知道信道信道关闭,才会结束循环
package main
import (
"fmt"
)
func getLess5(num chan int) {
for i := 0 ;i < 5; i++{
num <- i
}
close(num)
}
func main() {
ch := make(chan int)
// 在一个协程中 获取小于5的数字
go getLess5(ch)
count := 0
// 循环接受 信道里面的新值 直到close 关闭
for v := range ch{
count += v
}
fmt.Println(count)
}
缓冲信道
无缓冲信道的发送和接收过程是阻塞的,
我们还可以创建一个有缓冲(Buffer)的信道。只在缓冲已满的情况,才会阻塞向缓冲信道(Buffered Channel)发送数据。同样,只有在缓冲为空的时候,才会阻塞从缓冲信道接收数据。
ch := make(chan type, capacity)
缓冲信道capacity 表示缓冲信道的容量
package main
import (
"fmt"
)
func main() {
ch := make(chan string, 2)
ch <- "naveen"
ch <- "paul"
fmt.Println(<- ch)
fmt.Println(<- ch)
}
代码不会发生任何阻塞,什么时候会阻塞呢?
写入的时候,当缓冲区满的时候会阻塞,读取的时候,当缓冲区为空的时候,会阻塞
下面演示一下这个过程
package main
import (
"fmt"
"time"
)
func write(ch chan int){
for i := 0; i < 5 ;i++{
ch <- i
fmt.Printf("写入数据-%d\n",i)
}
close(ch)
}
func main() {
ch := make(chan int, 2)
go write(ch)
time.Sleep(time.Second) // 1 延时函数
for v:= range ch{
fmt.Printf("读取数据-%d\n",v)
time.Sleep(time.Second)
}
}
为什么加延时函数,不加延时函数,由于系统是并发的整个过程没法看清楚,
代码执行顺序
1.并发执行 go write(ch) 和 延时函数,延时函数,没有执行完毕,之前 write函数,向信道写入两次数据,之后由于信道的容量已经满了,所以不再向信道写入数据了
- 延时函数执行完毕后,for range 开始执行,这个时候,开始从信道读取数据,当读取一个数据后,信道的缓冲有多了1个单元
3.write 函数,可以向信道里面写入数据了,写入完成后,信道缓冲又满了, 此时继续等待
4.rang中的延时结束之后,就可以继续读取信道里面的值了,此过程循环,直到程序结束
计算容量和长度
缓冲信道的容量是指信道可以存储的值的数量。我们在使用 make 函数创建缓冲信道的时候会指定容量大小。
缓冲信道的长度是指信道中当前排队的元素个数。
func main() {
ch := make(chan int,3)
ch <- 1
fmt.Println(len(ch))
fmt.Println(cap(ch))
}