聊聊Golang中形形色色的同步原语

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

January 5, 2022 · 1 min

读写锁为什么那么快?(2)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.前言 在上一篇文章读写锁为什么那么快?(1)中,萌叔探讨了读写锁提高程序性能的原因。这一篇文章,萌叔将聊聊读写锁面临的2个问题。并看看标准库的是如何实现。 2.几个问题 Q1: 如何避免写锁饥饿或者读锁饥饿问题? 我们知道读写锁的特性:同一时刻允许多个线程(协程)对共享资源进行读操作, 也就是说多个线程(协程)可以多次加读锁。那么会出现这样一种情况,读锁还没有被完全释放的情况(读锁计数器大于0),又有新的读操作请求加读锁。这样写协程永远无法加上写锁,将会一直阻塞。 A1: 标准库实际上引入某种类似排队的机制 如上图所示,协程G1、G2、G3、G4并发访问共享资源。它们请求加锁的顺序为矩形的左边沿,耗时为矩形块的长度 t1时刻,G1加上了读锁,开始读操作 t2时刻,G2也加上了读锁,进行读操作 t3时刻,G3表示它要进行写操作 t4时刻,G4也想加读锁,并且现在共享资源上施加的读锁还没有完全释放(需要到t5时刻才能释放),但是由于G3已经表示它需要加写锁了,所以G4将会阻塞 所以实际的运行顺序是 Q2: 如何确保加读锁时,成本尽可能的低? 读写锁最适用的情况是读多写少的场景,降低读锁的加锁和释放锁的开销,才能提高整体的吞吐能力。 A2: 读协程和写协程互斥仅用的到int32的整数 萌叔虽然也使用condition实现了读写锁,但由于 condition要求在判断条件是否满足,以及修改条件中相关联的变量时,都需要加互斥锁,因此萌叔的实现性能和标准库有一定的差距。 写协程和写协程的互斥靠的是标准库的互斥锁 3.代码 标准库的实现很巧妙,阅读有一定的难度。萌叔将对部分代码进行注释,并讲解。 package sync import ( &quot;internal/race&quot; &quot;sync/atomic&quot; &quot;unsafe&quot; ) type RWMutex struct { w Mutex // held if there are pending writers writerSem uint32 // semaphore for writers to wait for completing readers /* 用于标识 1. 是否已经加了读锁或者写锁 2. 有多少个协程想加读锁(包含已经加上读锁的) */ readerSem uint32 // semaphore for readers to wait for completing writers readerCount int32 // number of pending readers /* 用于标识 写协程需要等待多少个读协程释放读锁才可以加上写锁 */ readerWait int32 // number of departing readers } const rwmutexMaxReaders = 1 &lt;&lt; 30 func (rw *RWMutex) RLock() { ... /* rw.readerCount+1 表示有读协程想加读锁 rw.readerCount &lt; 0 就说明当前共享资源已经被加了写锁,或者排队的时候,读操作排在了某个写操作请求的后面,那么需要把自己挂起在rw.readerSem上 */ if atomic.AddInt32(&amp;rw.readerCount, 1) &lt; 0 { // A writer is pending, wait for it. runtime_SemacquireMutex(&amp;rw.readerSem, false, 0) } ... } func (rw *RWMutex) RUnlock() { ... /* rw.readerCount 小于0 说明有写协程想要加写锁 则需要唤醒写协程 */ if r := atomic.AddInt32(&amp;rw.readerCount, -1); r &lt; 0 { // Outlined slow-path to allow the fast-path to be inlined rw.rUnlockSlow(r) } ... } func (rw *RWMutex) rUnlockSlow(r int32) { // A writer is pending. if atomic.AddInt32(&amp;rw.readerWait, -1) == 0 { // The last reader unblocks the writer. runtime_Semrelease(&amp;rw.writerSem, false, 1) } } func (rw *RWMutex) Lock() { ... // First, resolve competition with other writers. /* 写协程和写协程的互斥依靠标准库的互斥锁 */ rw.w.Lock() // Announce to readers there is a pending writer. /* */ r := atomic.AddInt32(&amp;rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders // Wait for active readers. /* 只要不等于0,就说明当前共享资源已经被加了读锁 那就把自己挂起在rw.writerSem上 */ if r != 0 &amp;&amp; atomic.AddInt32(&amp;rw.readerWait, r) != 0 { runtime_SemacquireMutex(&amp;rw.writerSem, false, 0) } ... } func (rw *RWMutex) Unlock() { ... // Announce to readers there is no active writer. r := atomic.AddInt32(&amp;rw.readerCount, rwmutexMaxReaders) ... // Unblock blocked readers, if any. /* 只要有读协程想要加读锁,就唤醒读协程 */ for i := 0; i &lt; int(r); i++ { runtime_Semrelease(&amp;rw.readerSem, false, 0) } // Allow other writers to proceed. rw.w.Unlock() ... } Golang的标准库的读写锁实现具有某种偏向性(偏向读操作)。 在施加写锁期间,只要有读协程表示想加读锁,等到写锁释放时。不管是否有其它写协程想要加写锁。都会优先把共享资源,让给读协程。 ...

June 1, 2021 · 2 min

读写锁为什么那么快?(1)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.实验 首先让我们来看一组单元测试结果,看看互斥锁和读写锁在不同场景下的表现。 这里使用了极客兔兔的《Go 语言高性能编程》中的case,在此特别鸣谢 vearne/golab BenchmarkReadMore-4 1 1195229024 ns/op BenchmarkReadMoreRW-4 9 122810085 ns/op BenchmarkReadMoreMyRW-4 8 145523134 ns/op BenchmarkWriteMore-4 1 1296582090 ns/op BenchmarkWriteMoreRW-4 1 1194438009 ns/op BenchmarkWriteMoreMyRW-4 1 1247918412 ns/op BenchmarkEqual-4 1 1344248968 ns/op BenchmarkEqualRW-4 2 661674534 ns/op BenchmarkEqualMyRW-4 2 685353924 ns/op 1.1 读写量 9:1 读多写少 方法 说明 耗时(ms) 备注 BenchmarkReadMore 标准库-互斥锁 1195 BenchmarkReadMoreRW 标准库-读写锁 122 BenchmarkReadMoreMyRW 萌叔实现的读写锁 145 传送门: vearne/second-realize/rwlock 1.2 读写量 1:9 写多读少 ...

June 1, 2021 · 1 min

聊聊Golang中的锁(2)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 前言 在 聊聊golang中的锁(1) 中,笔者提到Golang的锁,可能引发的非常高的CPU消耗,本文我们一起来探究一下,CPU时钟都消耗再了哪里。 2. 分析 修改代码, 使用pprof package main import ( &quot;fmt&quot; &quot;sync&quot; &quot;time&quot; _ &quot;net/http/pprof&quot; &quot;net/http&quot; &quot;log&quot; ) type LockBox struct{ sync.Mutex Value int } func deal(wpg *sync.WaitGroup, bp *LockBox, count int){ for i:=0;i&lt; count;i++{ bp.Lock() bp.Value++ bp.Unlock() } wpg.Done() } func main() { go func() { log.Println(http.ListenAndServe(&quot;:18081&quot;, nil)) }() timeStart := time.Now() workerCount := 100 taskCount := 10000000000 var wg sync.WaitGroup var box LockBox for i:=0;i&lt;workerCount;i++{ wg.Add(1) go deal(&amp;wg, &amp;box, taskCount/workerCount) } wg.Wait() fmt.Println(&quot;cost&quot;, time.Since(timeStart)) } 记录采样运行数据 ...

October 10, 2018 · 1 min

聊聊Golang中的锁(1)--可能造成较高的CPU消耗

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 前言 关于Golang中的锁的实现和源码早就文章阐述过了,这里我从实验的角度让大家看看对协程数量对任务执行速度,以及CPU开销的影响 运行环境 CPU: Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20GHz 8核 内存:16G GO version 1.10 协程数量分别是1, 2, 5, 10, 100, 200下完成1000000000次计数所花 总时间 CPU消耗 CPU峰值 分析 观察图可以发现, 协程数量是1的时候运行总时间反而最短,只耗时2.8秒,协程数量10时,运行总时间最长,耗时21.6秒 协程数量是100时,CPU峰值最大,为530%,协程数量是1是,CPU峰值最小,不到100% 代码 package main import ( &quot;fmt&quot; &quot;sync&quot; &quot;time&quot; ) type LockBox struct{ sync.Mutex Value int } func deal(wpg *sync.WaitGroup, bp *LockBox, count int){ for i:=0;i&lt; count;i++{ bp.Lock() bp.Value++ bp.Unlock() } wpg.Done() } func main() { timeStart := time.Now() //workerCount := 1 //workerCount := 2 workerCount := 200 taskCount := 1000000000 var wg sync.WaitGroup var box LockBox for i:=0;i&lt;workerCount;i++{ wg.Add(1) go deal(&amp;wg, &amp;box, taskCount/workerCount) } wg.Wait() fmt.Println(&quot;cost&quot;, time.Since(timeStart)) } 请我喝瓶饮料

October 8, 2018 · 1 min