博客主页:🏆看看是李XX还是李歘歘 🏆
🌺每天分享一些包括但不限于计算机基础、算法等相关的知识点🌺
💗点关注不迷路,总有一些📖知识点📖是你想要的💗
⛽️今天 浅聊goroutine和channel ⛽️💻💻💻
先看下面的一个小问题:
func main() { ch := make(chan string) ch <- "lcc" fmt.Println(<-ch)} |
报错:死锁:原因make后面没有带数字,新建的是无缓冲信道,此类信道只能用来流通数据,并不存储数据,接收一个数据,一定要及时将数据读取出去,不然就会发现阻塞,那么加一个数字即可建立缓冲信道:
func main() { ch := make(chan string,1) ch <- "lcc" fmt.Println(<-ch)} |
或者:加一个协程,将数据读出去?然并卵,还是报错
func main() { ch := make(chan string) ch <- "lcc" go func() { fmt.Println(<-ch) }()} |
原因:在执行到写入channel就已经发生了死锁,都等不到你去开新的协程,那就将开协程内容前移到写内容之前,让他先候着?有点东西了,不报错了,但是没有输出啊
func main() { ch := make(chan string) go func() { fmt.Println(<-ch) }() ch <- "lcc"} |
原因:协程结束的时间顺序,main也是一个协程,那么匿名函数的协程和main协程哪个先结束了呢?如果main协程先结束,那么没有结果就很正常了啊,怎么办?先让main睡一会
func main() { ch := make(chan string) go func() { fmt.Println(<-ch) }() ch <- "lcc" time.Sleep(time.Millisecond)} |
有了有了有了。
Goroutine
和进程和线程一样,Goroutine也是提高程序并发的一种手段,Goroutine就是代码中使用go关键词创建的执行单元,也是大家熟知的有“轻量级线程”之称的协程,协程是不为操作系统所知的,它由编程语言层面实现,上下文切换不需要经过内核态,再加上协程占用的内存空间极小,所以有着非常大的发展潜力。
那么协程间的通信方式是什么呢?这就引出了大名鼎鼎的channel,汉译“通道”
channel
使用make可以为channel分配缓存,使用new不行,也就是一开始问题提到的加数字,建立缓存信道。
通道(channel),协程通信方式,协程之间共享同一个线程的内存是真的共享内存吗?不是,可以通过go语言并发编程的座右铭理解:使用通信来共享内存,而不是通过共享内存来通信。channel可能是促使Go使用CSP(Communicating Sequential Processes)作为并发模型的一个很重要原因,毕竟CSP的核心观念也是使用通道将两个并发执行的实体连接起来(todo后面讲,我还没学明白)。
channel是否带有缓存,是否会阻塞读写,可以通过make中参数进行判断。
无缓冲的通道(unbuffered channel),没有能力保存任何值,必须读取双方同时做好准备,一方没有做好准备就会造成死锁,channel只做为流通的通道,该通道是同步的(协程同步方法之一)。
ch := make(chan string) |
有缓冲的通道(buffered channel),有一定能力存储数据,在channel没满之前可以一直接收,在channel没空之前可以一直取出,该通道是异步的。
ch := make(chan string,capacity) |
可以使用close关闭channel,已经关闭的channel只能读不能再写数据,对于 nil的 channel,无论收发都会被阻塞。当然也可以设置通道为只读或者只写。
接下来讨论三个channel不常为人知的一面
源码:
//在 src/runtime/chan.go
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
type hchan struct { qcount uint // 当前队列中剩余元素个数 dataqsiz uint // 环形队列长度,即可以存放的元素个数 buf unsafe.Pointer // 环形队列指针 elemsize uint16 // 每个元素的大小 closed uint32 // 标识关闭状态 elemtype *_type // 元素类型 sendx uint // 队列下标,指示元素写入时存放到队列中的位置 recvx uint // 队列下标,指示元素从队列的该位置读出 recvq waitq // 等待读消息的goroutine队列 sendq waitq // 等待写消息的goroutine队列 lock mutex // 互斥锁,chan不允许并发读写 }
channel储存的单位是一个指针指向的array,这个array的功能就是一个环形队列,也就是说是index = index % length(array) 得出来的结果。 而且最下面的lock是一个互斥锁,证明了一个chan每次只能在一个goruntine中使用。所以使用chan通信当然不用加锁了,因为go底层已经加上了。
对已经关闭的的chan进行读写,会怎么样?为什么?
答案:众所周知,已经关闭的channel只能读不能再写数据,具体如下:
- 读已经关闭的 chan 能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。
- 如果 chan 关闭前,buffer 内有元素还未读 , 会正确读到 chan 内的值,且返回的第二个 bool 值(是否读成功)为 true。
- 如果 chan 关闭前,buffer 内有元素已经被读完,chan 内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel 元素的零值,但是第二个 bool 值一直为 false。
- 写已经关闭的 chan 会 panic
为什么写已经关闭的 chan
就会 panic
呢?
//在 src/runtime/chan.go
func chansend(c *hchan,ep unsafe.Pointer,block bool,callerpc uintptr) bool {
//省略其他
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
//省略其他
}
- 当
c.closed != 0
则为通道关闭,此时执行写,源码提示直接panic
,输出的内容就是上面提到的"send on closed channel"
。
为什么读已关闭的 chan 会一直能读到值?
func chanrecv(c *hchan,ep unsafe.Pointer,block bool) (selected,received bool) {
//省略部分逻辑
lock(&c.lock)
//当chan被关闭了,而且缓存为空时
//ep 是指 val,ok := <-c 里的val地址
if c.closed != 0 && c.qcount == 0 {
if receenabled {
raceacquire(c.raceaddr())
}
unlock(&c.lock)
//如果接受之的地址不空,那接收值将获得一个该值类型的零值
//typedmemclr 会根据类型清理响应的内存
//这就解释了上面代码为什么关闭的chan 会返回对应类型的零值
if ep != null {
typedmemclr(c.elemtype,ep)
}
//返回两个参数 selected,received
// 第二个采纳数就是 val,ok := <- c 里的 ok
//也就解释了为什么读关闭的chan会一直返回false
return true,false
}
}
-
c.closed != 0 && c.qcount == 0
指通道已经关闭,且缓存为空的情况下(已经读完了之前写到通道里的值) - 如果接收值的地址
ep
不为空
- 那接收值将获得是一个该类型的零值
-
typedmemclr
会根据类型清理相应地址的内存 - 这就解释了上面代码为什么关闭的 chan 会返回对应类型的零值
对未初始化的的chan进行读写,会怎么样?为什么?
答案:读写未初始化的 chan
都会阻塞
为什么写nil的的 chan
就会 panic
呢?
//在 src/runtime/chan.go中
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil {
// 不能阻塞,直接返回 false,表示未发送成功
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 省略其他逻辑
}
未初始化的 chan
此时是等于 nil。
- 当它不能阻塞的情况下,直接返回
false
,表示写chan
失败 - 当
chan
能阻塞的情况下,则直接阻塞gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
, 然后调用throw(s string)
抛出错误,其中waitReasonChanSendNilChan
就是"chan send (nil chan)"
报错
为什么读nil的的 chan
就会 panic
呢?
//在 src/runtime/chan.go中
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
//省略逻辑...
if c == nil {
if !block {
return
}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
//省略逻辑...
}
未初始化的 chan
此时是等于 nil
。
- 当它不能阻塞的情况下,直接返回
false
,表示读chan
失败 - 当
chan
能阻塞的情况下,则直接阻塞gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
, 然后调用throw(s string)
抛出错误,其中waitReasonChanReceiveNilChan
就是"chan receive (nil chan)"
报错
对已满的channel进行写
写不进去,会报错
对没有内容读channel进行读,相当于nil状态
会panic
设置单方向的 channel
默认情况下,通道是双向的,也就是,既可以往里面发送数据也可以从里面接收数据。
但是,我们经常见一个通道作为参数进行传递而值希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,这时候我们可以指定通道的方向。
单向 channel 变量的声明非常简单,如下:
var ch1 chan int // ch1是一个正常的channel,不是单向的
var send chan<- float64 // ch2 是单向 channel,只用于写 float64 数据
var recv <-chan int // ch3 是单向 channel,只用于读取 int 数据
- chan<- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
- <-chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。
可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通 channel:
c := make(chan int, 3)
var send chan<- int = c // send-only
var recv <-chan int = c // receive-only
d1 := (chan int)(send) //cannot convert send (type chan<- int) to type chan int
d2 := (chan int)(recv) //cannot convert recv (type <-chan int) to type chan int
问题:从单个的channel取数据可以使用遍历,那么如何从多个channel存取出数据?答案是:通道的多路复用select
通道的多路复用select
注意:select或range遍历空channel会死锁。
package main
import (
"fmt"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for {
select {
case c1 := <-ch1:
fmt.Println(c1)
case c2 := <-ch2:
fmt.Println(c2)
}
}
}()
ch1 <- 1
ch1 <- 2
}
关于select:
- 每个 case 都必须是一个通信。
由于 select 语句是专为通道设计的,所以每个 case 表达式中都只能包含操作通道的表达式,比如接收表达式。- 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。
- 如果多个 case 都不能运行,若有 default 子句,则执行该语句,反之,select 将阻塞,直到某个 case 可以运行。
- 所有 channel 表达式都会被求值。
如果通道没有数据发送,但 select 中有存在接收通道数据的语句;或者空的select,将发生死锁:
package main
import "fmt"
type name interface{}
func main() {
lcc := make(chan int,3)
select {
case i:=<-lcc:
fmt.Println(i)
}
}package main
type name interface{}
func main() {
select {}
}以上两段代码会造成死锁。
select 语句只能对其中的每一个case表达式各求值一次。所以,如果想连续或定时地操作其中的通道的话,就需要通过在for语句中嵌入select语句的方式实现,并用标签退出for循环。
package main
import "fmt"
func main() {
lcc := make(chan int,3)
lcc<-1
lcc<-2
lcc<-3
LOOP:
for {
select {
case i:=<-lcc:
fmt.Println(i)
default:
break LOOP
}
}
}