博客
关于我
Go语言中的互斥锁和读写锁(Mutex和RWMutex)
阅读量:413 次
发布时间:2019-03-06

本文共 3888 字,大约阅读时间需要 12 分钟。

Go语言中的锁机制与死锁场景分析

在Go语言的并发编程中,虽然Channel机制是协程间通信的主要工具,但在某些场景下,使用锁是更直观、易于理解的选择。Go语言提供了两种主要的锁类型:互斥锁(Mutex)和读写锁(RWMutex)。本文将详细介绍这两种锁的使用方法,并分析常见的死锁场景。


一、Mutex(互斥锁)

1.1 互斥锁的基本概念

Mutex的意思是“互斥锁”,即同一时间只能有一个协程执行保护其代码的区域。使用Mutex只需要掌握Lock和Unlock两个方法即可。Lock会阻塞当前协程,直到锁被释放;Unlock则由持有锁的协程释放锁。

1.2 不加锁的示例

在没有加锁的情况下,十个协程同时对一个共享变量进行操作,可能会导致以下问题:

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()
for j := 0; j < 10000; j++ {
count++
}
}()
}
wg.Wait()
fmt.Println(count)
}

问题分析:由于协程间没有互斥机制,十个协程可能同时修改count变量,导致结果远小于预期(如上述示例中结果为38532,而非100000)。

1.3 加锁的示例

在加锁后,可以避免多个协程同时修改共享变量,从而确保数据的线性化:

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()
for j := 0; j < 10000; j++ {
mu.Lock()
count++
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println(count)
}

问题分析:通过Mutex锁,每个协程在加锁和解锁之间保护了count变量的修改,确保了线性化,结果为100000。


二、RWMutex(读写锁)

2.1 读写锁的优势

Mutex在大量并发场景下可能会导致高阻塞率和性能问题。RWMutex则通过支持多个读者同时持有锁,提升了读操作的并发度,特别适合读密度高、写操作少的场景。

2.2 读写锁的规则

RWMutex遵循以下规则:

  • 读锁可重入:在已经持有读锁的情况下,继续加读锁不会阻塞。
  • 写锁具有优先级:在有读锁存在时,写锁会阻塞,直到所有读锁解锁。
  • 写锁和读锁互斥:在持有写锁时,其他协程无法获取读锁或写锁。
  • 2.3 读写锁的方法

    RWMutex提供了以下方法:

    • Lock():加写锁,阻塞直到解锁。
    • Unlock():释放锁。
    • RLock():加读锁,非阻塞。
    • RUnlock():释放读锁。

    2.4 并发读示例

    以下示例显示了多个读协程如何在不影响彼此的情况下同时读取数据:

    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")
    }

    运行结果:可以看到,三个读协程可以并行执行,而不会互相阻塞。

    2.5 并发读写示例

    以下示例展示了读写混合场景下的性能:

    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")
    }

    运行结果:这种配置下,读写混合的并发效率较高,适合大部分场景的需求。


    三、死锁场景

    3.1 Lock/Unlock不是成对出现

    如果Lock和Unlock不成对出现,可能会导致资源无法释放,从而引发死锁。例如:

    mu.Lock()
    defer mu.Unlock()
    // 可能在此处发生意外,导致Unlock无法执行

    问题分析:如果在加锁后发生panic或其他异常,Unlock方法不会被执行,导致锁被死持有。

    3.2 锁被拷贝使用

    在某些场景下,锁可能被多次拷贝使用,导致锁无法被正确释放:

    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")
    }

    问题分析:外层函数已经加锁,在拷贝后又加锁,导致外层的Unlock无法释放锁,这是一个死锁。

    3.3 循环等待

    类似哲学家就餐问题的场景,A等待B,B等待C,C等待A:

    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()
    // 依赖B
    muB.Lock()
    defer muB.Lock()
    }()
    go func() {
    defer wg.Done()
    muB.Lock()
    defer muB.Lock()
    // 依赖A
    muA.Lock()
    defer muA.Unlock()
    }()
    wg.Wait()
    }

    问题分析:协程A和B相互等待对方的锁,导致死锁。


    通过以上内容,可以看出,Go语言的锁机制虽然提供了强大的工具,但在使用时仍需注意潜在问题,以避免死锁等并发安全问题。

    转载地址:http://qzlkz.baihongyu.com/

    你可能感兴趣的文章
    NSJSON的用法(oc系统自带的解析方法)
    查看>>
    nslookup 的基本知识与命令详解
    查看>>
    NSNumber与NSInteger的区别 -bei
    查看>>
    NSOperation基本操作
    查看>>
    NSRange 范围
    查看>>
    NSSet集合 无序的 不能重复的
    查看>>
    NSURLSession下载和断点续传
    查看>>
    NSUserdefault读书笔记
    查看>>
    NS图绘制工具推荐
    查看>>
    NT AUTHORITY\NETWORK SERVICE 权限问题
    查看>>
    NT symbols are incorrect, please fix symbols
    查看>>
    ntelliJ IDEA 报错:找不到包或者找不到符号
    查看>>
    NTFS文件权限管理实战
    查看>>
    ntko web firefox跨浏览器插件_深度比较:2019年6个最好的跨浏览器测试工具
    查看>>
    ntko文件存取错误_苹果推送 macOS 10.15.4:iCloud 云盘文件夹共享终于来了
    查看>>
    ntp server 用法小结
    查看>>
    ntpdate 通过外网同步时间
    查看>>
    ntpdate同步配置文件调整详解
    查看>>
    NTPD使用/etc/ntp.conf配置时钟同步详解
    查看>>
    NTP及Chrony时间同步服务设置
    查看>>