博客主页:🏆​看看是李XX还是李歘歘​ 🏆

🌺每天分享一些包括但不限于计算机基础、算法等相关的知识点🌺

💗点关注不迷路,总有一些📖知识点📖是你想要的💗

⛽️今天  浅聊goroutine和channel   ⛽️💻💻💻

浅聊goroutine和channel_java

先看下面的一个小问题:

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)"​​报错

浅聊goroutine和channel_开发语言_02

 为什么读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)"​​报错

浅聊goroutine和channel_后端_03

对已满的channel进行写

写不进去,会报错

浅聊goroutine和channel_sed_04

对没有内容读channel进行读,相当于nil状态

会panic

浅聊goroutine和channel_后端_05

设置单方向的 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
}
}
}


浅聊goroutine和channel_数据_06