聊聊Golang中形形色色的同步原语
版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | https://vearne.cc
注意: 本文基于Golang 1.17.1
1.引言
阅读Golang的源码的话,会发现里面有形形色色的同步原语
,发挥着重要的作用。这篇文章,萌叔打算把它们之间的依赖关系,以及与Linux的同步原语
的对应关系整理一下。
2. 与Linux系统的同步原语
之间的对应关系
锁类型 | Golang | Linux |
---|---|---|
互斥锁 | runtime.mutex (低阶) sync.Mutex (高阶) |
#include <pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); |
信号量 | runtime.semaphore | #include <sys/sem.h> int semget(key_t key, int nsems, int semflg); int semctl(int semid, int semnum, int cmd, ...); int semop(int semid, struct sembuf *sops, size_t nsops); |
条件变量 | sync.Cond | #include <pthread.h> int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond); |
3.依赖关系
3.1 runtime.mutex
func lock(l *mutex) {
lockWithRank(l, getLockRank(l))
}
func unlock(l *mutex) {
unlockWithRank(l)
}
下图中
注意:
1)红色部分是Linux提供的系统调用futex, 全称是fast user-space locking
2)只有在互斥锁发生竞争的场景,才会真的执行futex
相关的系统调用。
3.2 runtime.semaphore
//go:linkname sync_runtime_Semacquire sync.runtime_Semacquire
func sync_runtime_Semacquire(addr *uint32) {
semacquire1(addr, false, semaBlockProfile, 0)
}
//go:linkname sync_runtime_Semrelease sync.runtime_Semrelease
func sync_runtime_Semrelease(addr *uint32, handoff bool, skipframes int) {
semrelease1(addr, handoff, skipframes)
}
下图中
1)Treap是树堆,是一种平衡二叉树。
2)runtime.gopark()用于协程挂起
3)runtime.goready()把协程的状态从_Gwaiting
变成_Grunnable
,并放入就绪队列。
注意:
1) runtime.semaphore
依赖低阶同步原语runtime.mutex
。
2) runtime.semaphore
可能会引入自旋
3.3 sync.Mutex
注意:
1) sync.Mutex
依赖低阶同步原语runtime.semaphore
。
2) sync.Mutex
可能会引入自旋
3.4 sync.Cond
注意:
1) sync.Cond
的成员变量L
是接口,并通常使用sync.Mutex
2) sync.Cond
依赖低阶同步原语runtime.mutex
。
4.总结
仔细的看完Golang的各种同步原语后,萌叔有几个感受
4.1 Golang中的同步原语大多数能够找到对应的Linux系统调用
由于Linux系统调用通常开销很大,为了实现中极力的避免使用Linux系统调用,Golang在语言层面(用户空间)重新实现了这些系统调用。
由于Golang的runtime实现了对协程的调度,因此通过使用gopark()
和goready()
等函数,即可挂起协程或者换入协程,同样达到同步的效果。因此大多数情况,Golang中同步原语的操作是完全不需要内核的介入的。
4.2 高阶的同步原语的实现依赖低阶的同步原语
就像在3.4图中看到的sync.Cond
依赖sync.Mutex
和runtime.mutex
。高阶同步原语的实现往往依赖低阶同步原语。其中在Linux中,高阶同步原语也是依赖低阶同步原语的,只不过这些高阶同步原语使用的场景相对比较特殊而已。
5. 参考资料
1.futex
2.详解linux多线程——互斥锁、条件变量、读写锁、自旋锁、信号量
3.mutex
4.sched_yield
5.go中semaphore(信号量)源码解读
6.semget
7.semaphore的原理与实现
8.pthread_cond_wait
6. 后记
Golang为了避免使用锁,还有一些独特的技巧。比如同一个P上的多个G,竞争访问某些临界数据时,可以完全不用加锁。
- P上的多个G共用一个runq (runnable queue存放就绪状态的G)
- P上的多个G共用一个pool (sync.Pool)