Fork me on GitHub

版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | 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

runtime/lock_futex.go

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

runtime/sema.go

//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.Mutexruntime.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)

请我喝瓶饮料

微信支付码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注