并发编程

context包

context包核心有4个API

  • context.WithValue:设置键值对,并返回一个新的content实例
  • context.WithCancel
  • context.WithDeadline
  • context.WithTimeout 三者都返回一个可以取消的context实例和取消函数

context实例是不可变的,每一次都是新创建

第一个常安全传递数据,只有我这个线程能访问我这个变量

后面用于控制链路

scss 复制代码
func TestParent(t *testing.T) {
    ctx := context.Background()
    parent := context.WithValue(ctx, "key", "value")
    child := context.WithValue(parent, "key2", "value5")
    t.Log(parent.Value("key"))
    t.Log(child.Value("key2"))
    t.Log(parent.Value("key"))
}

当key相同的时候 子会覆盖父的值 子如果没有会继承父

父级是没有办法拿到子的值的

sync包

加锁

mutex可以看做是锁,而RWMutex则是读写锁

一般的用法是将Mutex或者RWMutex和需要被保护的资源封装一个结构体内

  • 如果有多个goroutine同时读写的资源,就一定要保护起来
  • 如果多个goroutine只读某个资源,那就不需要保护
csharp 复制代码
type safeResource struct{
 resource interface{}
 lock sync.Mutex

}

使用锁的时候,优先使用RWMutex

  • RWMutex:核心就是RLock,RUnlick,Lock,Unlock
  • Mutex:Lock和Unlock

Mutex细节

  • 自旋作为快路径
  • 等待队列作为慢路径

锁:本质上是一个状态 自旋:就是一个循环状态 ,原子操作 ,为了调整自己的状态(等待和饥饿);

自旋可以通过控制次数或者时间来退出循环

慢路径和语言特性相关,有些依赖于操作系统线程调度

成功

一把锁,没有人持有,也没有人抢,那么一个cas操作就能成功,一次性的自旋

为什么要有正常模式和饥饿模式

原先有一堆排列等待的goroutine

如果有一个新的goroutine进来争夺锁,而且队列里面也有等待的,你是设计者,你把锁给谁?

  1. 给等待的:我们要保证公平,先到先得
  2. 公平竞争:保证效率 新的肯定已经占用了cpu,大概率会得到锁

正常模式:就是新旧竞争的模式,避免goroutine的调度

那如果每次来都对新的抢走,怎么办?

饥饿模式

如果等待的时间超过1ms,锁就会变成饥饿模式,直接会交给原来等待的goroutine

要么队列只剩一个goroutine,要么队列的goroutine等待小于1ms,则退出饥饿

总结

  1. 先上来一个cas操作,如果这把锁空闲,并且没人抢,那么直接成功
  2. 否则,自旋几次,成功不加入队列
  3. 否则加入队列
  4. 从队列中唤醒
    • 正常模式:和新来的一起抢锁,大概率失败
    • 饥饿模式: 肯定拿到锁

解锁

加锁状态才能解锁

注意事项

  • 适合读多写少的场景
  • 写多读少不如直接加写锁
  • Mutex和RWMutex都是不可重入的,加了锁在释放之前不能再次加锁
  • 尽可能用defer来解锁,避免panic

Once

确保某个动作至多执行一次

必须要传递指针,或者包含的是指针

go 复制代码
type Do struct {
    once sync.Once
}

func (d *Do) Init(){
    d.once.Do(func() {
       
    })
}

是一种double-check的变种

没有直接利用读写锁,利用原子操作

Pool

用的时候记得还

  • 先查看自己是否有资源,有则直接返回

  • 没有创建新的

  • GC的时候会释放缓存的资源

  • 一般情况考虑缓存资源

  • 复用内存

    • 减少内存分配,减轻GC压力
    • 少消耗cpu资源

pool细节

TLB therad-local-buffer

每个线程搞一个队列,再来一个共享队列

  • 每一个p一个poolLocal对象
  • 每个poolLocal有一个private和shared
  • shared指向的是一个poolChain,poolChain的数据会被别的p偷走
  • poolChain是一个链表+ring buffer的双重结构
    • 整体上一个双向列表
    • 单个节点指向一个ringbuffer,后一个是前面的两倍

ringbuffer优势: -一次性分配好内存,循环利用 对缓存友好

获取步骤

  • private可用就直接返回
  • 不可用从自己的poolChain里面获取一个
    • 从头开始 ,头是最近创建的ringbuffer
    • 从队头往队尾开始
  • 找不到就从别的P里面偷一个,偷的是全局并发的,理论上来讲别的P能恰好一起
    • 偷是从队尾开始
  • 如果偷也偷不到 就去找缓刑的victim
  • 如果缓刑也没有,那么自己创建

Put步骤

  • private 要是没放东西,就直接放 private
  • 否则,准备放 poolChain
    • 如果 poolChain 的 HEAD 还没创建,就创建一个HEAD,然后创建一个8容量的 ring buffer,把数据丢过去
    • 如果 pooIChain 的 HEAD 指向的 ring bufter 没满则丢过去 ring buffer
    • 如果 poolChain 的 HEAD 指向的 ring buffer 已经满了,就创建一个新的节点,并且创建一个两倍容量的ring buffer,把数据丢过去

GC对pool的影响

纯粹的依赖GC,用户完全无法手工控制 核心依赖

  • lcoals
  • victim

GC

  • locals会被挪过去变成victim
  • victim会被直接回收到

复活:如果victim的对象再次被使用,那么就会被丢回local,逃过下一轮GC回收的命运

优点: 防止GC引起的性能抖动

为什么不先找缓刑,先偷别人的,还是全局竞争的?

因为sync.Pool希望victime里面的对象尽可能的被回收掉,垃圾回收尽可能的回收

要点

  • sync.Pool 和 GC 的关系:数据默认在 local 里面,GC 的时候会被挪过去 victim 里面。如果这时候有P 用了 victim 的数据,那么数据会被放回去 local 里面。
  • poolChain 的设计:核心在于理解 poolChain 是一个双向链表加 ring buffer 的双重结构。

由这两个核心衍生出来的各种问题:

  • 什么时候 P 会用 victim 的数据:偷都偷不到的时候。
  • 为什么 Go 会设计这种结构?一个全局共享队列不好吗?这个问题要结合 TLB 来回答,TLB 解决全局 锁竞争的方案,Go 结合自身 P这么一个优势,设计出来的。
  • 窃取:这个可以作为一个刷亮点的东西,结合 GMP 调度里面的工作窃取,原理都是一样的。
  • 使用 sync.Pool 有什么注意点(缺点、优点)?高版本的 Go 里面的 sync.Pool 没特别大的缺点,硬要说就是内存使用量不可控,以及 GC 之后即便可以用 victim,Get 的速率还是要差点。

waitgroup

同步多个goroutine之间的工作

拆分给多个goroutine执行任务,在完成后需要合并结果或者需要等所有小任务都完成

在进入goroutine之前,先+1,完成任务就-1

  • +多了会导致wait一直阻塞,引起goroutine泄露
  • -多了导致panic

实现

  • 当前一共有多少个任务还没完成
  • 当前多少个goroutine调用了wait方法
  • 需要一个东西来进行协调

WaitGroup:

  • state1:在 64 位下,高 32 位记录了还有多少任务在运行;低 32 位记录了有多少 goroutine 在等 Wait()方法返回
  • state2:信号量,用于挂起或者唤醒goroutine,约等于 Mutex 里面的 sema 字段要注意横向对比)

本质上,WaitGroup 是一个无锁实现,严重依赖于CAS 对 state1 的操作。

channel

  1. 用于传递数据-队列

  2. 利用阻塞行;可以间接控制goroutine或者其他资源的消耗。有点想令牌桶机制

  • channel有没有缓冲
  • 谁在发
  • 谁在收
  • 关了没

无缓冲

  • 要求两端都得有goroutine ,否则就是阻塞

有缓冲

  • 没满或者没空之前都不会阻塞,但是满了或者空了就会阻塞

对于发送者,只要发出的数据没有地方放,就是阻塞

对于接受者,尝试拿数据但是没有拿到就是阻塞

发布订阅的模式:发布者不断往里面塞数据,订阅者从管道取数据

缺陷

  • 没有消费组概念,不能说同一个事件被多个goroutine同时消费,有且只有一个
  • 无法回退,也无法随机消费

代码实现并发消息队列

  • 方案1:每一个订阅者创建一个channel
  • 方案2: 一个channel我去遍历订阅者
相关推荐
研究司马懿4 小时前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
梦想很大很大17 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰1 天前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘1 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤1 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt112 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto5 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室6 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题6 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo