Go 并发 -- 信道
这是『就要学习 Go 语言』系列的第 22 篇分享文章
上篇文章讲了关于协程的一些用法,比如如何创建协程、匿名协程等。这篇文章我们来讲讲信道。
信道是协程之间通信的管道,从一端发送数据,另一端接收数据。
信道声明
使用信道之前需要声明,有两种方式:
var c chan int // 方式一
c := make(chan int) // 方式二
使用关键字 chan 创建信道,声明时有类型,表明信道只允许该类型的数据传输。信道的零值为 nil。方式一就声明了 nil 信道。nil 信道没什么作用,既不能发送数据也不能接受数据。方式二使用 make 函数创建了可用的信道 c。
func main() {c := make(chan int)fmt.Printf("c Type is %T\n",c)fmt.Printf("c Value is %v\n",c)
}
输出:
c Type is chan int
c Value is 0xc000060060
上面的代码创建了信道 c,而且只允许 int 型数据传输。一般将信道作为参数传递给函数或者方法实现两个协程之间通信,有没有注意到信道 c 的值是一个地址,传参的时候直接使用 c 的值就可以,而不用取址。
信道的使用
读写数据
Go 提供了语法方便我们操作信道:
c := make(chan int)
// 写数据
c <- data // 读数据
variable <- c // 方式一
<- c // 方式二
读写数据注意信道的位置,信道在箭头的左边是写数据,在右边是从信道读数据。上面的方式二读数据是合理的,读出来的数据丢弃不使用。
注意:信道操作默认是阻塞的,往信道里写数据之后当前协程便阻塞,直到其他协程将数据读出。一个协程被信道操作阻塞后,Go 调度器会去调用其他可用的协程,这样程序就不会一直阻塞。信道的这种特性非常有用,接下来我们就可以看到。
我们来温习下上篇文章的一个例子:
func printHello() {fmt.Println("hello world goroutine")
}func main() {go printHello()time.Sleep(1*time.Second)fmt.Println("main goroutine")
}
这个例子 main() 协程使用了 time.Sleep() 函数休眠了 1s 等待 printHello() 执行完成。很黑科技,在生产环境绝对不可以这样用。我们使用信道修改下:
func printHello(c chan bool) {fmt.Println("hello world goroutine")<- c // 读取信道的数据
}func main() {c := make(chan bool)go printHello(c)c <- true // main 协程阻塞fmt.Println("main goroutine")
}
输出:
hello world goroutine
main goroutine
上面的例子,main 协程创建完 printHello 协程之后,第 8 行往信道 c 写数据,main 协程阻塞,Go 调度器调度可使用 printHello 协程,从信道 c 读出数据,main 协程接触阻塞继续运行。注意:读取操作没有阻塞是因为信道 c 已有可读的数据,否则,读取操作会阻塞。
死锁
前面提到过,读/写数据的时候信道会阻塞,调度器会去调度其他可用的协程。问题来了,如果没有其他可用的协程会发生什么情况?没错,就会发生著名的死锁。最简单的情况就是,只往信道写数据。
func main() {c := make(chan bool)c <- true // 只写不读fmt.Println("main goroutine")
}
报错:
fatal error: all goroutines are asleep - deadlock!
同理,只读不写也会报同样的错误。
关闭信道与 for loop
发送数据的信道有能力选择关闭信道,数据就不能传输。数据接收的时候可以返回一个状态判断该信道是否关闭:
val, ok := <- channel
val 是接收的值,ok 标识信道是否关闭。为 true 的话,该信道还可以进行读写操作;为 false 则标识信道关闭,数据不能传输。
使用内置函数 close() 关闭信道。
func printNums(ch chan int) {for i := 0; i < 10; i++ {ch <- i}close(ch)
}func main() {ch := make(chan int)go printNums(ch)for {v, ok := <-chif ok == false { // 通过 ok 判断信道是否关闭fmt.Println(v, ok)break}fmt.Println(v, ok)}
}
输出:
0 true
1 true
2 true
3 true
0 false
printNums 协程写完数据之后关闭了信道,在 main 协程里对 ok 判断,若为 false 说明信道关闭,退出 for 循坏。从关闭的信道读出来的值是对应类型的零值,上面最后一行的输出值是 int 类型的零值 0。
使用 for 循环,需要手动判断信道有没有关闭。如果嫌烦的话,那就使用 for range 读取信道吧,信道关闭,for range 自动退出。
func printNums(ch chan int) {for i := 0; i < 4; i++ {ch <- i}close(ch)
}func main() {ch := make(chan int)go printNums(ch)for v := range ch {fmt.Println(v)}
}
输出:
0
1
2
3
提一点,使用 for range 一个信道,发送完毕之后必须 close() 信道,不然发生死锁。
缓冲信道和信道容量
之前创建的信道是无缓冲的,读写信道会立马阻塞当前协程。对于缓冲信道,写不会阻塞当前信道直到信道满了,同理,读操作也不会阻塞当前信道除非信道没数据。创建带缓冲的信道:
ch := make(chan type, capacity)
capacity 是缓冲大小,必须大于 0。 内置函数 len()、cap() 可以计算信道的长度和容量。
func main() {ch := make(chan int,3)ch <- 7ch <- 8ch <- 9//ch <- 10 // 注释打开的话,协程阻塞,发生死锁会发生死锁:信道已满且没有其他可用信道读取数据fmt.Println("main stopped")
}
输出:main stopped
创建了缓冲为 3 的信道,写入 3 个数据时信道不会阻塞。如果将第 7 行代码注释打开的话,此时信道已满,协程阻塞,又没有其他可用协程读数据,便发生死锁。
再来看个例子:
func printNums(ch chan int) {ch <- 7ch <- 8ch <- 9fmt.Printf("channel len:%d,capacity:%d\n",len(ch),cap(ch))fmt.Println("blocking...")ch <- 10 // 阻塞close(ch)
}func main() {ch := make(chan int,3)go printNums(ch)// 休眠 2stime.Sleep(2*time.Second)for v := range ch {fmt.Println(v)}fmt.Println("main stopped")
}
输出:
channel len:3,capacity:3
blocking...
7
8
9
10
main stopped
休眠 2s 的目的是让信道写满数据发生阻塞,从打印结果可以看出。2s 之后,主协程从信道读取数据,信道容量有余阻塞便解除,继续写数据。
如果缓冲信道是关闭状态但有数据,仍然可以读取数据:
func main() {ch := make(chan int,3)ch <- 7ch <- 8//ch <- 9close(ch)for v := range ch {fmt.Println(v)}fmt.Println("main stopped")
}
输出:
7
8
main stopped
单向信道
之前创建的都是双向信道,既能发送数据也能接收数据。我们还可以创建单向信道,只发送或者只接收数据。
语法:
rch := make(<-chan int)
sch := make(chan<- int)
rch 是只发送信道,sch 是只接受信道。
这种单向信道有什么用呢?我们总不能只发不接或只接不发吧。这种信道主要用在信道作为参数传递的时候,Go 提供了自动转化,双向转单向。
重写之前的例子:
func printNums(ch chan<- int) {for i := 0; i < 4; i++ {ch <- i}close(ch)
}func main() {ch := make(chan int)go printNums(ch)for v := range ch {fmt.Println(v)}
}
输出:
0
1
2
3
main 协程中 ch 是一个双向信道,printNums() 在接收参数的时候将 ch 自动转成了单向信道,只发不收。但在 main 协程中,ch 仍然可以接收数据。使用单向通道主要是可以提高程序的类型安全性,程序不容易出错。
信道数据类型
信道是一类值,类似于 int、string 等,可以像其他值一样在任何地方使用,比如作为结构体成员、函数参数、函数返回值,甚至作为另一个通道的类型。我们来看下使用通道作为另一个通道的数据类型。
func printWord(ch chan string) {fmt.Println("Hello " + <-ch)
}func productCh(ch chan chan string) {c := make(chan string) // 创建 string type 信道ch <- c // 传输信道
}func main() {// 创建 chan string 类型的信道ch := make(chan chan string)go productCh(ch)// c 是 string type 的信道c := <-chgo printWord(c)c <- "world"fmt.Println("main stopped")
}
输出:
Hello world
main stopped
希望这篇文章能够帮助你,Good day!
Go 并发 -- 信道相关推荐
- go channel 缓冲区最大限制_一起攻克面试难关:Go 面试每天一篇(第 40 天)
你好哇,欢迎来答题,一起来看下昨天题目的解析: 1.关于无缓冲和有冲突的channel,下面说法正确的是? A. 无缓冲的channel是默认的缓冲为1的channel: B. 无缓冲的channel ...
- 5G LTE窄带物联网(NB-IoT) 9
第6章 媒体访问控制子层 MAC子层是直接与PHY子层接口的最低子层.因此,该子层几乎实时地执行其功能.该子层的功能如下[26]: 随机访问和争用解决程序. 将多个RLC PDU多路复用和解复用到单个 ...
- RabbitMQ 入门系列(2)— 生产者、消费者、信道、代理、队列、交换器、路由键、绑定、交换器
本系列是「RabbitMQ实战:高效部署分布式消息队列」和 「RabbitMQ实战指南」书籍的读书笔记. RabbitMQ 中重要概念 1. 生产者 生产者(producer)创建消息,然后发送到代理 ...
- Go 学习笔记(24)— 并发(03)[通道特点、通道声明、通道发送/接收/关闭、单向通道]
1. 通道概念 chan 是 Go 语言里面的一个关键宇,是 channel 的简写,翻译为中文就是通道. goroutine 是 Go 语言里面的并发执行体,通道是 goroutine 之间通信和同 ...
- Go 学习笔记(22)— 并发(01)[进程、线程、协程、并发和并行、goroutine 启动、goroutine 特点,runtime 包函数]
Go 语言通过编译器运行时( runtime ),从语言上支持了并发的特性. 虽然 Go 程序编译后生成的是本地可执行代码,但是这些可执行代码必须运行在Go 语言的运行时(Runtime )中.Go ...
- Go 系列教程 —— 20. 并发入门
欢迎来到我们 Golang 系列教程的第 20 篇. Go 是并发式语言,而不是并行式语言.在讨论 Go 如何处理并发之前,我们必须理解何为并发,以及并发与并行的区别. 并发是什么? 并发是指立即处理 ...
- golang并发编程goroutine+channel(一)
go语言的设计初衷除了在不影响程序性能的情况下减少复杂度,另一个目的是在当今互联网大量运算下,如何让程序的并发性能和代码可读性达到极致.go语言的并发关键词 "go" go dos ...
- Go 语言编程 — 并发 — Channel 通道
目录 文章目录 目录 Channel 通道缓冲区 遍历通道与关闭通道 Channel channel(通道)是用来传递数据的一个数据结构. 通道可用于两个 goroutine 之间通过传递一个指定类型 ...
- golang 并发与并行学习笔记(三)
Go语言并发的设计模式和应用场景 以下设计模式和应用场景来自Google IO上的关于Goroutine的PPT:https://talks.golang.org/2012/concurrency.s ...
最新文章
- 你知道Spring是怎么解析配置类的吗?
- 剑指offer-栈的压入、弹出序列
- boost::statechart模块实现延迟错误的测试程序
- LeetCode 239. Sliding Window Maximum
- 这几个动态规划的问题,面试官就爱问
- U-net,及其和FCN的区别
- 卡方检验的统计量推导_卡方检验的卡方检验法的基本原理和步骤
- 【YOLOX训练部署】YOLOX训练自己的VOC数据集
- stm32的人体红外传感器的初步使用
- 一道疯狂bypass的题目
- 重拾Java基础知识:设计模式
- 记:css绘制小猪佩奇的项目及踩过的坑
- HDFS文件的读写操作理论解析
- Oracle对象——视图之简单视图与视图约束
- ixgbe 驱动安装
- hyperledger cello部署
- Harry Potter and the Half-Blood Prince
- 只网签没备案 房管局能查到吗_如何查询已网签且未备案的房产
- 富文本编辑vue插件vue-quill-editor
- Chrom谷歌浏览器配置vue插件
热门文章
- 外企工作的你需要了解的印度种姓制度
- 香港科技大学计算机专业博士申请,协助申请研究生MSc博士PhD,香港高校【计算机2021提前批】已经开放,含【港府奖学金】...
- python键盘监听及模拟键盘输入
- 教资照片可以用计算机么,2015教资国考|图像、声音、视频的加工【专项练习二】...
- [洛谷]P1914小书童——凯撒密码
- 手把手教你收集产品情报
- HTML5新增属性nofollow标签的应用场景
- 创客教育中的空间设计实物原理
- 关于IO流和String常见的一些面试题
- 迅为嵌入式linux驱动开发笔记(八)—内核定时器