作者:dcguo,腾讯 CSIG 电子签开放平台中心
分享 Golang 并发基础库,扩展以及三方库的一些常见问题、使用介绍和技巧,以及对一些并发库的选择和优化探讨。
go 原生/扩展库
提倡的原则
不要通过共享内存进行通信;相反,通过通信来共享内存。
Goroutine
goroutine 并发模型
调度器主要结构
主要调度器结构是 M,P,G
M,内核级别线程,goroutine 基于 M 之上,代表执行者,底层线程,物理线程
P,处理器,用来执行 goroutine,因此维护了一个 goroutine 队列,里面存储了所有要执行的 goroutine,将等待执行的 G 与 M 对接,它的数目也代表了真正的并发度( 即有多少个 goroutine 可以同时进行 );
G,goroutine 实现的核心结构,相当于轻量级线程,里面包含了 goroutine 需要的栈,程序计数器,以及所在 M 的信息
P 的数量由环境变量中的 GOMAXPROCS 决定,通常来说和核心数对应。
映射关系
用户空间线程和内核空间线程映射关系有如下三种:
N:1
1:1
M:N
调度图
关系如图,灰色的 G 则是暂时还未运行的,处于就绪态,等待被调度,这个队列被 P 维护
![905f728420590a1dbe32ebc54c0d4af0.png](https://i-blog.csdnimg.cn/blog_migrate/8317526217fe6ed69b21ac4890ef4bc2.png)
注: 简单调度图如上,有关于 P 再多个 M 中切换,公共 goroutine 队列,M 从线程缓存中创建等步骤没有体现,复杂过程可以参考文章简单了解 goroutine 如何实现。
goroutine 使用
demo1
go list.Sort()
demo2
func Announce(message string, delay time.Duration) { go func() { time.Sleep(delay) fmt.println(message) }() }
channel
channel 特性
创建
// 创建 channel
a := make(chan int)
b := make(chan int, 10)
// 单向 channel
c := make(chan<- int)
d := make(<-chan int)
存入/读取/关闭
![79537d2139c29a7353579cb3e4740574.png](https://i-blog.csdnimg.cn/blog_migrate/2093dee588afa8910d9991d6ea60b840.png)
tip:
v, ok := <-a // 检查是否成功关闭(ok = false:已关闭)
channel 使用/基础
use channel
ci := make(chan int) cj := make(chan int, 0) cs := make(chan *os.File, 100)
c := make(chan int) go func() { list.Sort() c <- 1 }() doSomethingForValue <- c
func Server(queue chan *Request) { for req := range queue { sem <- 1 go func() { process(req) <- sem }() } }
func Server(queue chan *Requet) { for req := range queue { sem <- 1 go func(req *Request) { process(req) <- sem }(req) } }
func Serve(queue chan *Request) { for req := range queue { req := req sem <- 1 go func() { process(req) <-sem }() } }
channel 使用/技巧
等待一个事件,也可以通过 close 一个 channel 就足够了。
c := make(chan bool)
go func() {
// close 的 channel 会读到一个零值
close(c)
}()
<-c
阻塞程序
开源项目【是一个支持集群的 im 及实时推送服务】里面的基准测试的案例
![752ff0784d4b9174d13810fe777ffdd1.png](https://i-blog.csdnimg.cn/blog_migrate/df86f0d0a777b9f624e1d718ec60cd08.png)
取最快结果
func main() {
ret := make(chan string, 3)
for i := 0; i < cap(ret); i++ {
go call(ret)
}
fmt.Println(<-ret)
}
func call(ret chan<- string) {
// do something
// ...
ret <- "result"
}
协同多个 goroutines
注: 协同多个 goroutines 方案很多,这里只展示 channel 的一种。
limits := make(chan struct{}, 2)
for i := 0; i < 10; i++ {
go func() {
// 缓冲区满了就会阻塞在这
limits <- struct{}{}
do()
<-limits
}()
}
搭配 select 操作
for {
select {
case a := <- testChanA:
// todo a
case b, ok := testChanB:
// todo b, 通过 ok 判断 tesChanB 的关闭情况
default:
// 默认分支
}
}
main go routinue 确认 worker goroutinue 真正退出的方式
func worker(testChan chan bool) {
for {
select {
// todo some
// case ...
case <- testChan:
testChan <- true
return
}
}
}
func main() {
testChan := make(chan bool)
go worker(testChan)
testChan <- true
<- testChan
}
关闭的 channel 不会被阻塞
testChan := make(chan bool)
close(testChan)
zeroValue := <- testChan
fmt.Println(zeroValue) // false
testChan <- true // panic: send on closed channel
注: 如果是 buffered channel
, 即使被 close, 也可以读到之前存入的值,读取完毕后开始读零值,写入则会触发 panic
nil channel 读取和存入都不会阻塞,close 会 panic
略
range 遍历 channel
for range
c := make(chan int, 20)
go func() {
for i := 0; i < 10; i++ {
c <- i
}
close(c)
}()
// 当 c 被关闭后,取完里面的元素就会跳出循环
for x := range c {
fmt.Println(x)
}