并发编程

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我去遍历订阅者
相关推荐
我是前端小学生6 小时前
Go语言中的方法和函数
go
探索云原生10 小时前
在 K8S 中创建 Pod 是如何使用到 GPU 的: nvidia device plugin 源码分析
ai·云原生·kubernetes·go·gpu
自在的LEE17 小时前
当 Go 遇上 Windows:15.625ms 的时间更新困局
后端·kubernetes·go
Gvto2 天前
使用FakeSMTP创建本地SMTP服务器接收邮件具体实现。
go·smtp·mailtrap
白泽来了2 天前
【Go进阶】手写 Go websocket 库(一)|WebSocket 通信协议
开源·go
witton2 天前
将VSCode配置成Goland的视觉效果
ide·vscode·编辑器·go·字体·c/c++·goland
非凡的世界2 天前
5个用于构建Web应用程序的Go Web框架
golang·go·框架·web
湫qiu2 天前
6.5840 Lab-Key/Value Server 思路
后端·go
我是前端小学生3 天前
Go语言中的init函数
go
我是前端小学生3 天前
Go语言中内部模块的可见性规则
go