Go语言中的互斥锁和读写锁(Mutex和RWMutex)
<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>一、Mutex(互斥锁)<ul><li>不加锁示例</li><li>加锁示例</li></ul></li><li>二、RWMutex(读写锁)<ul><li>并发读示例</li><li>并发读写示例</li></ul></li><li>三、死锁场景<ul><li>Lock/Unlock不是成对出现</li><li>锁被拷贝使用</li><li>循环等待</li></ul></li></ul></div><br>虽然Go语言提供channel来保证协程的通信,但是某些场景用锁来显示保证协程的安全更清晰易懂。<br>
Go语言中主要有两种锁,互斥锁Mutex和读写锁RWMutex,下面分别介绍一下使用方法,以及出现死锁的常见场景。<p></p>
<h1 id="一mutex互斥锁">一、Mutex(互斥锁)</h1>
<p>Mutex是互斥锁的意思,也叫排他锁,同一时刻一段代码只能被一个线程运行,使用只需要关注方法Lock(加锁)和Unlock(解锁)即可。<br>
在Lock()和Unlock()之间的代码段称为资源的临界区(critical section),是线程安全的,任何一个时间点都只能有一个goroutine执行这段区间的代码。</p>
<h2 id="不加锁示例">不加锁示例</h2>
<p>先来一段不加群的代码,10个协程同时累加1万</p>
<pre><code>package main
import (
"fmt"
"sync"
)
func main() {
var count = 0
var wg sync.WaitGroup
//十个协程数量
n := 10
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
//1万叠加
for j := 0; j < 10000; j++ {
count++
}
}()
}
wg.Wait()
fmt.Println(count)
}
</code></pre>
<p>运行结果如下</p>
<pre><code>38532
</code></pre>
<p>正确的结果应该是100000,这里出现了并发写入更新错误的情况</p>
<h2 id="加锁示例">加锁示例</h2>
<p>我们再添加锁,代码如下</p>
<pre><code>package main
import (
"fmt"
"sync"
)
func main() {
var count = 0
var wg sync.WaitGroup
var mu sync.Mutex
//十个协程数量
n := 10
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
//1万叠加
for j := 0; j < 10000; j++ {
mu.Lock()
count++
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println(count)
}
</code></pre>
<p>运行结果如下,可以看到,已经看到结果变成了正确的100000<br>
<img src="https://img2020.cnblogs.com/blog/662544/202011/662544-20201102215521301-1435321942.png" alt="" loading="lazy"></p>
<h1 id="二rwmutex读写锁">二、RWMutex(读写锁)</h1>
<p>Mutex在大量并发的情况下,会造成锁等待,对性能的影响比较大。<br>
如果某个读操作的协程加了锁,其他的协程没必要处于等待状态,可以并发地访问共享变量,这样能让读操作并行,提高读性能。<br>
RWLock就是用来干这个的,这种锁在某一时刻能由什么问题数量的reader持有,或者被一个wrtier持有</p>
<p>主要遵循以下规则 :</p>
<ol>
<li>读写锁的读锁可以重入,在已经有读锁的情况下,可以任意加读锁。</li>
<li>在读锁没有全部解锁的情况下,写操作会阻塞直到所有读锁解锁。</li>
<li>写锁定的情况下,其他协程的读写都会被阻塞,直到写锁解锁。</li>
</ol>
<p>Go语言的读写锁方法主要有下面这种</p>
<ol>
<li>Lock/Unlock:针对写操作。<br>
不管锁是被reader还是writer持有,这个Lock方法会一直阻塞,Unlock用来释放锁的方法</li>
<li>RLock/RUnlock:针对读操作<br>
当锁被reader所有的时候,RLock会直接返回,当锁已经被writer所有,RLock会一直阻塞,直到能获取锁,否则就直接返回,RUnlock用来释放锁的方法</li>
</ol>
<h2 id="并发读示例">并发读示例</h2>
<pre><code>package main
import (
"fmt"
"sync"
"time"
)
func main() {
var m sync.RWMutex
go read(&m, 1)
go read(&m, 2)
go read(&m, 3)
time.Sleep(2 * time.Second)
}
func read(m *sync.RWMutex, i int) {
fmt.Println(i, "reader start")
m.RLock()
fmt.Println(i, "reading")
time.Sleep(1 * time.Second)
m.RUnlock()
fmt.Println(i, "reader over")
}
</code></pre>
<p>运行如下<br>
<img src="https://img2020.cnblogs.com/blog/662544/202011/662544-20201103113830655-414668107.png" alt="" loading="lazy"></p>
<p>可以看到,3的读还没结束,1和2已经开始读了</p>
<h2 id="并发读写示例">并发读写示例</h2>
<pre><code>package main
import (
"fmt"
"sync"
"time"
)
var count = 0
func main() {
var m sync.RWMutex
for i := 1; i <= 3; i++ {
go write(&m, i)
}
for i := 1; i <= 3; i++ {
go read(&m, i)
}
time.Sleep(1 * time.Second)
fmt.Println("final count:", count)
}
func read(m *sync.RWMutex, i int) {
fmt.Println(i, "reader start")
m.RLock()
fmt.Println(i, "reading count:", count)
time.Sleep(1 * time.Millisecond)
m.RUnlock()
fmt.Println(i, "reader over")
}
func write(m *sync.RWMutex, i int) {
fmt.Println(i, "writer start")
m.Lock()
count++
fmt.Println(i, "writing count", count)
time.Sleep(1 * time.Millisecond)
m.Unlock()
fmt.Println(i, "writer over")
}
</code></pre>
<p>运行结果如下<br>
<img src="https://img2020.cnblogs.com/blog/662544/202011/662544-20201103113839887-22762933.png" alt="" loading="lazy"></p>
<p>如果我们可以明确区分reader和writer的协程场景,且是大师的并发读、少量的并发写,有强烈的性能需要,我们就可以考虑使用读写锁RWMutex替换Mutex</p>
<h1 id="三死锁场景">三、死锁场景</h1>
<p>当两个或两个以上的进程在执行过程中,因争夺资源而处理一种互相等待的状态,如果没有外部干涉无法继续下去,这时我们称系统处于死锁或产生了死锁。<br>
死锁主要有以下几种场景。</p>
<h2 id="lockunlock不是成对出现">Lock/Unlock不是成对出现</h2>
<p>没有成对出现容易会出现死锁的情况,或者是Unlock 一个未加锁的Mutex而导致 panic,代码建议以下面紧凑的方式出现</p>
<pre><code>mu.Lock()
defer mu.Unlock()
</code></pre>
<h2 id="锁被拷贝使用">锁被拷贝使用</h2>
<pre><code>package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()
copyTest(mu)
}
//这里复制了一个锁,造成了死锁
func copyTest(mu sync.Mutex) {
mu.Lock()
defer mu.Unlock()
fmt.Println("ok")
}
</code></pre>
<p>在函数外层已经加了一个Lock,在拷贝的时候又执行了一次Lock,因此这是一个永远不会获得的锁,因为外层函数的Unlock无法执行。</p>
<h2 id="循环等待">循环等待</h2>
<p>A等待B,B等待C,C等待A,陷入了无限循环(哲学家就餐问题)</p>
<pre><code>package main
import (
"sync"
)
func main() {
var muA, muB sync.Mutex
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
muA.Lock()
defer muA.Unlock()
//A依赖B
muB.Lock()
defer muB.Lock()
}()
go func() {
defer wg.Done()
muB.Lock()
defer muB.Lock()
//B依赖A
muA.Lock()
defer muA.Unlock()
}()
wg.Wait()
}
</code></pre>
<p>以上就是Go语言的锁使用,由chenqionghe倾情整理,giao~</p><br><br>
来源:https://www.cnblogs.com/chenqionghe/p/13919427.html
頁:
[1]