无缓冲阻塞 chan 杂谈

chan类似队列版管道,无缓冲chan看起来好像是全局变量,通过它可让多个goroutine间通信。 这其实隐含一个事实,chan阻塞会引发goroutine上下文切换,而切换到哪一个可执行goroutine由go调度器决定(与阻塞chan相关)。go当前能够使用的goroutine,必须在其待命队列中,否则会产生死锁。

上下文切换

多进程多线程都具备上下文切换,即保存恢复现场的能力。goroutine的上下文切换实现,是在用户态基础上进行,只不过它涉及到的资源比线程更少,如产生一个线程系统调用分配内存通常在1M,而goroutine只有2kb,此外在使用寄存器,段位上,goroutine也只需3个左右,而线程则通常在10个左右。

无缓冲阻塞

go调度器对goroutine的使用配合chan,具有有序性(在高并发访问对象时,可用chan这种特性让访问请求隐性排队,解决竞态问题)。main函数是特殊的入口goroutine,若有阻塞代码,运行时runtime会寻找已入队列的goroutine并在适当的时机调用它。chan并不是全局变量,确切来说它的读/写阻塞会触发当前goroutine执行权转移,它只是个通信器。好似打电话,必须先知道对方号码并有连线,才能正常工作,若顺序不对,表现在golang中便是死锁

Blocking

package main

import (
    "fmt"
)

func f1(in chan int) {
    fmt.Println(<-in)
}

func main() {
    out := make(chan int)
    out <- 2
    go f1(out)
}

上述代码会产生死锁,main入口goroutine,通道out产生了发送阻塞,此时runtime会尝试调度与out通道读相关的goroutine执行,但可惜的是,在 out <- 2之前,并没有向go执行器队列加入与out读相关的goroutine。换句话而言,f1压根就没入队,没有执行机会。

unblocking

 package main

 import "fmt"

 func main() {
     out := make(chan int)
     go f1(out)
     // 此处顺序大有讲究,在使用发送通道之前必需想好数据接收的退路,f1即是
     out <- 2
 }

 func f1(in chan int) {
     fmt.Println(<-in)
 }

chan vs 全局变量

上文提到chan类似管道,管道顾名思义一端进一端出,很形象表明了一个连接器。go中的chan连接goroutine,游离于众多goroutine之间,功用性与全局变量有得一拼。但chan绝对不是全局变量,一个全局变量,可以在同一函数体内重复读写,但对无缓冲chan而言是不可以,原因在同一goroutine内对同一chan读写时,存在读或写阻塞面临切换上下文,另一个对应的永远没执行机会,如下

  • 无缓冲通道死锁
 package main
 import "fmt"

 func main() {
     ch := make(chan int)
     ch <- 5
     fmt.Println(<-ch)
 }
  • 有缓冲通道正常
 package main

 import "fmt"

 func main() {
     ch := make(chan int, 1)
     ch <- 5
     fmt.Println(<-ch)
 }

有缓冲通道,意味着在未超过当前通道限制数之前,当前的goroutine是非阻塞,不会发生上下文切换,即当前goroutine的控制权不发生转移,runtime也就不会去寻求其它相关goroutine执行。

小结

  • 无缓冲chan 进和出都会阻塞.
  • 有缓冲chan 先进先出队列, 出会一直阻塞到有数据, 进时当队列未满不会阻塞, 队列已满则阻塞.
本作品采用《CC 协议》,转载必须注明作者和本文链接
pardon110
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
开发者 @ 社科大
文章
134
粉丝
24
喜欢
101
收藏
55
排名:106
访问:8.9 万
私信
所有博文
社区赞助商