golang-运行时-并发编程

本文介绍了Go语言中Goroutine的同步机制,包括context的使用、sync包的各种同步原语,如Mutex、RWMutex的锁机制,以及WaitGroup、Once和Cond的协同工作方式。特别讨论了Mutex的饥饿模式和RWMutex的读写操作细节,同时提到了系统监控和网络轮询器在并发控制中的作用。
摘要由CSDN通过智能技术生成

Context

在多个 Goroutine 组成的树中同步取消信号以减少对资源的消耗和占用

  • context.Background
  • context.TODO
  • context.WithDeadline
  • context.WithValue

同步原语与锁

sync.Mutex
sync.RWMutex
sync.WaitGroup
sync.Once
sync.Cond

CAS(compare and swap)

是一条cpu的原子指令, cpu进行比较两个值是否相等, 然后原子的更新某个位置的值.

sync.Mutex 互斥锁
  • 正常模式:
    锁的等待者会按照先进先出的顺序获取锁

  • 饥饿模式:
    一旦 Goroutine 超过 1ms 没有获取到锁,它就会将当前互斥锁切换饥饿模式,防止部分 Goroutine 被『饿死』。互斥锁会直接交给等待队列最前面的 Goroutine。

在获取锁时如果已经被锁了,就会通过自旋等方式等待锁的释放.
自旋期间会一直占用cpu时间

RWMutex 读写互斥锁

细粒度的互斥锁, 不限制读的并发, 但是读写, 写写操作无法并发.

写操作:
sync.RWMutex.Lock 和 sync.RWMutex.Unlock
加锁

  1. 阻塞后续的写操作, 其他Goroutine在获取写锁是会进入自旋或休眠
  2. 阻塞后续读操作
  3. 如果还有Goroutine持有读锁, 当前Goroutine会进入休眠, 等待所有读锁释放后通过信号量将其唤醒.

解锁

  1. 释放读锁
  2. 释放所有因获取读锁而陷入等待的Goroutine
  3. 释放写锁

读操作:
sync.RWMutex.RLock 和 sync.RWMutex.RUnlock
加锁

  1. 如果有其他Goroutine获得了写锁, 那么久陷入休眠等待锁释放
  2. 没有Goroutine获得写锁, 获取读锁

解锁

  1. 没有其他Goroutine获取写锁, 释放读锁
  2. 如果有其他Goroutine持有写锁, 释放读锁,如果是最后一个持有读锁的会触发信号量, 调度器会唤醒尝试获取写锁的Goroutine
WaitGroup

sync.WaitGroup 可以等待一组 Goroutine 的返回

sync.Once

保证在 Go 程序运行期间的某段代码只会执行一次

sync.Cond

可以让一组的 Goroutine 都在满足特定条件时被唤醒

ErrGroup

为一组 Goroutine 中提供了同步、错误传播以及上下文取消的功能

  • 只有第一个错误会被返回, 剩余的错误会被丢弃
Semaphore 信号量

获取资源
semaphore.Weighted.Acquire 阻塞
semaphore.Weighted.TryAcquire 非阻塞
释放资源
semaphore.Weighted.Release

SingleFlight

在一个服务中抑制对下游的多次重复请求(如redis缓存击穿)

定时器

time.Timer
轮询力度10ms

Channel

  • 先从 Channel 读取数据的 Goroutine 会先接收到数据
  • 先向 Channel 发送数据的 Goroutine 会得到先发送数据的权利
type hchan struct {
	qcount   uint   // channel 中元素个数
	dataqsiz uint   // 循环队列的长度
	buf      unsafe.Pointer // 缓冲区数据指针
	elemsize uint16 // 收发的元素大小
	closed   uint32
	elemtype *_type // 收发的元素类型
	sendx    uint   // 发送操作处理到的位置
	recvx    uint   // 接收操作处理到的位置
	recvq    waitq  // 阻塞的接收Goroutine 列表
	sendq    waitq  // 阻塞的发送Goroutine 列表

	lock mutex
}
发送数据

发送数据对当前channel加锁, 防止并发修改数据

  1. 如果当前 Channel 的 recvq 上存在已经被阻塞的 Goroutine,那么会直接将数据发送给当前 Goroutine 并将其设置成下一个运行的 Goroutine
  2. 如果 Channel 存在缓冲区并且其中还有空闲的容量,我们会直接将数据存储到缓冲区 sendx 所在的位置上
  3. 如果不满足上面的两种情况,会创建一个 runtime.sudog 结构并将其加入 Channel 的 sendq 队列中,当前 Goroutine 也会陷入阻塞等待其他的协程从 Channel 接收数据
接收数据
  1. 如果 Channel 为空,那么会直接调用 runtime.gopark 挂起当前 Goroutine
  2. 如果 Channel 已经关闭并且缓冲区没有任何数据,runtime.chanrecv 会直接返回
  3. 如果 Channel 的 sendq 队列中存在挂起的 Goroutine,会将 recvx 索引所在的数据拷贝到接收变量所在的内存空间上并将 sendq 队列中 Goroutine 的数据拷贝到缓冲区
  4. 如果 Channel 的缓冲区中包含数据,那么直接读取 recvx 索引对应的数据
  5. 在默认情况下会挂起当前的 Goroutine,将 runtime.sudog 结构加入 recvq 队列并陷入休眠等待调度器的唤醒
关闭管道

当 Channel 是一个空指针或者已经被关闭时, 会panic

调度器

任务窃取(编译器在函数调用时插入函数)
抢占式调度(gc时基于信号量)

G-M-P 模型
  • G 表示Goroutine,它是一个待执行的任务
  • M — 表示操作系统的线程,它由操作系统的调度器调度和管理
  • P — 表示处理器,它可以被看做运行在线程上的本地调度器
G
// 部分字段
type g struct {
	stack       stack   // 栈内存范围
	stackguard0 uintptr // 用于调度器抢占式调度
	
	preempt       bool // 抢占信号
	preemptStop   bool // 抢占时将状态修改成 `_Gpreempted`
	preemptShrink bool // 在同步安全点收缩栈
	
	_panic       *_panic // 最内侧的 panic 结构体
	_defer       *_defer // 最内侧的延迟函数结构体
	
	m              *m   // 当前 Goroutine 占用的线程,可能为空
	sched          gobuf    // 存储 Goroutine 的调度相关的数据
	atomicstatus   uint32   // Goroutine 的状态
	goid           int64    // Goroutine 的 ID
}

  • 等待中
  • 可运行
  • 运行中
M

最多只会有 GOMAXPROCS 个活跃线程能够正常运行

type m struct {
	g0   *g // 持有调度栈的特殊Goroutine, 深度参与运行时的调度过程
	curg *g // 当前线程上运行的用户 Goroutine
	p             puintptr  // 运行代码的处理器
	nextp         puintptr  // 暂存的处理器
	oldp          puintptr  // 执行系统调用之前使用线程的处理器
	...
}
P

线程和 Goroutine 的中间层, 提供线程需要的上下文环境, 也负责调度线程上等待执行的Goroutine队列.
P 的数量等于 GOMAXPROCS 的数量

调度器启动

创建M 和 P

创建 Goroutine
  • 初始化 Goroutine 结构体
  • 运行队列
    • 本地P队列
    • 调度器全局队列
  • 调度信息
调度
  • 为了保证公平,当全局运行队列中有待执行的 Goroutine 时,通过 schedtick 保证有一定几率会从全局的运行队列中查找对应的 Goroutine
  • 从处理器本地的运行队列中查找待执行的 Goroutine
  • 阻塞地从本地运行队列、全局运行队列中查找
  • 从网络轮询器中查找是否有 Goroutine 等待运行
  • 通过 runtime.runqsteal 尝试从其他随机的处理器中窃取待运行的 Goroutine,该函数还可能窃取处理器的计时器
  • 获取Goroutine 并执行后, 再次出发新一轮的Goroutine调度

触发调度的时机:

  • Goroutine主动挂起
  • 系统调用
  • Goroutine执行结束退出
  • 协作式调度
  • 信号抢占式调度

网络轮训器(netpoll)

运行时用来处理 I/O 操作的关键组件
网络轮询器并不是由运行时中的某一个线程独立运行的,运行时的调度器和系统调用都会通过 runtime.netpoll 与网络轮询器交换消息,获取待执行的 Goroutine 列表,并将待执行的 Goroutine 加入运行队列等待处理。

系统监控(sysmon)

它在内部启动了一个不会中止的循环,在循环的内部会轮询网络、抢占长期运行或者处于系统调用的 Goroutine 以及触发垃圾回收
系统监控在创建的线程(M)上执行, 不需要处理器§

  • 检查死锁
  • 运行计时器 — 获取下一个需要被触发的计时器
  • 轮询网络 — 获取需要处理的到期文件描述符
  • 抢占处理器 — 抢占运行时间较长的或者处于系统调用的 Goroutine
  • 垃圾回收 — 在满足条件时触发垃圾收集回收内存
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值