​Golang 并发编程指南

本文深入探讨了 Golang 的并发编程,包括 goroutine 的模型、调度、使用,以及 channel 的特性和技巧。文章介绍了如何利用 channel 实现并发控制、同步和通信,同时讨论了 Go 的基础并发库如 Mutex、Atomic、WaitGroup 等。此外,还提到了并发编程的实践案例,如批量校验、历史数据标签和批量操作,以及并发控制和错误处理的策略。最后,文章提出了并发编程中的思考,如合适的 goroutine 数量、锁的使用以及避免 goroutine 泄露的方法。
摘要由CSDN通过智能技术生成

ab8c18ab0f7d94a15ee234816c53e0f8.gif

作者:dcguo,腾讯 CSIG 电子签开放平台中心

分享 Golang 并发基础库,扩展以及三方库的一些常见问题、使用介绍和技巧,以及对一些并发库的选择和优化探讨。

go 原生/扩展库

提倡的原则

不要通过共享内存进行通信;相反,通过通信来共享内存。

Goroutine
goroutine 并发模型
调度器主要结构

主要调度器结构是 M,P,G

  1. M,内核级别线程,goroutine 基于 M 之上,代表执行者,底层线程,物理线程

  2. P,处理器,用来执行 goroutine,因此维护了一个 goroutine 队列,里面存储了所有要执行的 goroutine,将等待执行的 G 与 M 对接,它的数目也代表了真正的并发度( 即有多少个 goroutine 可以同时进行 );

  3. G,goroutine 实现的核心结构,相当于轻量级线程,里面包含了 goroutine 需要的栈,程序计数器,以及所在 M 的信息

P 的数量由环境变量中的 GOMAXPROCS 决定,通常来说和核心数对应。

映射关系

用户空间线程和内核空间线程映射关系有如下三种:

  1. N:1

  2. 1:1

  3. M:N

调度图

关系如图,灰色的 G 则是暂时还未运行的,处于就绪态,等待被调度,这个队列被 P 维护

905f728420590a1dbe32ebc54c0d4af0.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

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
取最快结果
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)
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值