无缓冲阻塞 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
开发者 @ 社科大
文章
135
粉丝
24
喜欢
103
收藏
56
排名:105
访问:8.9 万
私信
所有博文
社区赞助商