Go 语言核心 36 讲
郝林
《Go 并发编程实战》作者,前轻松筹大数据负责人
79610 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 55 讲
Go 语言核心 36 讲
15
15
1.0x
00:00/00:00
登录|注册

26 | sync.Mutex与sync.RWMutex

不要在多个函数之间直接传递互斥锁
不要对尚未锁定或已解锁的互斥锁解锁
不要忘记解锁
不要重复锁定
读锁获取方式
异同
注意事项
用途
互斥锁和读写锁的指针类型实现的接口
sync.RWMutex
sync.Mutex
同步工具
临界区
竞态条件
思考题
sync包
竞态条件、临界区与同步工具
并发编程知识关系脑图

该思维导图由 AI 生成,仅供参考

我在前面用 20 多篇文章,为你详细地剖析了 Go 语言本身的一些东西,这包括了基础概念、重要语法、高级数据类型、特色语句、测试方案等等。
这些都是 Go 语言为我们提供的最核心的技术。我想,这已经足够让你对 Go 语言有一个比较深刻的理解了。
从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包。这会涉及这些代码包的标准用法、使用禁忌、背后原理以及周边的知识。
既然 Go 语言是以独特的并发编程模型傲视群雄的语言,那么我们就先来学习与并发编程关系最紧密的代码包。

前导内容: 竞态条件、临界区与同步工具

我们首先要看的就是sync包。这里的“sync”的中文意思是“同步”。我们下面就从同步讲起。
相比于 Go 语言宣扬的“用通讯的方式共享数据”,通过共享数据的方式来传递信息和协调线程运行的做法其实更加主流,毕竟大多数的现代编程语言,都是用后一种方式作为并发编程的解决方案的(这种方案的历史非常悠久,恐怕可以追溯到上个世纪多进程编程时代伊始了)。
一旦数据被多个线程共享,那么就很可能会产生争用和冲突的情况。这种情况也被称为竞态条件(race condition)这往往会破坏共享数据的一致性。
共享数据的一致性代表着某种约定,即:多个线程对共享数据的操作总是可以达到它们各自预期的效果。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入介绍了Go语言中的sync.Mutex和sync.RWMutex,这两个类型是sync包中最重要且常用的同步工具。文章首先解释了竞态条件、临界区和同步工具的概念,强调了在多线程共享数据时可能出现的问题,以及同步工具的重要性。接着详细介绍了互斥锁的作用和使用方法,包括锁定和解锁操作的示例代码,并列举了使用互斥锁时需要注意的事项。重点强调了在并发编程中如何使用互斥锁来保护临界区,以及使用互斥锁的注意事项。文章还提到了避免重复锁定和解锁互斥锁的重要性,以及对互斥锁的传递和副本问题。总的来说,本文内容详实,适合读者快速了解并发编程中的同步工具以及互斥锁的基本原理和使用方法。 读写锁是读/写互斥锁的简称,它可以对读操作和写操作施加不同程度的保护,实现更加细腻的访问控制。文章还介绍了读写锁的特点和使用方法,以及与互斥锁的异同。最后,强调了正确使用读写锁的重要性,以及解锁未被锁定的锁可能引发的panic。 总的来说,本文内容涵盖了并发编程中的重要概念和同步工具,以及互斥锁和读写锁的使用方法和注意事项,对于读者快速了解并发编程中的同步工具和锁的特点具有很高的参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Go 语言核心 36 讲》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(49)

  • 最新
  • 精选
  • Geek_cd5dcf
    讲的通俗易懂,还是挺好理解的,想问下mutex如果加锁后 mutex.lock() defer mutex.unlock() 在所有场景下都不会出错吗?

    作者回复: 对,不会,这是defer的机制保证的。

    2018-10-11
    6
    21
  • 🐻
    1. Locker 接口 2. func (rw *RWMutex) RLocker() Locker

    作者回复: √

    2019-03-16
    16
  • Pana
    如果cpu只有一个核心,是不是就不会产生并发的情况?

    作者回复: 如果CPU只有一个核心,那么就不会有真正的并行计算了。但是,并发还是会有的。这是因为仍然可以同时有多个goroutine存在(它们可以同时处于可运行状态),只不过Go语言的运行时系统无法让它们在同一时刻都运行罢了。 并发和并行这两个词的含义是不同的,需要我们分清楚。简单来说,并发是指在同一个时间段内提交多个任务给系统,并行是指在同一时刻系统能执行多个任务。

    2020-03-08
    3
    13
  • 授人以🐟,不如授人以渔
    老师,麻烦分析一下这样的场景:main goroutine 拿到读锁,此时 goroutine 1 试图拿到写锁但被阻塞,紧接着 goroutine 2 试图拿到读锁。我想知道 goroutine 2 为什么也会被阻塞,另外 main goroutine 读锁被释放后,哪个 goroutine 会继续运行?

    作者回复: 1. 我又看了下源码。这是为了避免“迭代读锁定”的问题。这个问题最终会导致当前读写锁永不可用。你想想,如果一个 goroutine 一直在不断地读锁定同一个读写锁,那么想要写锁定这个读写锁的 goroutine 就会永远阻塞在那里。 2. main goroutine 释放读锁之后,goroutine 1 会首先得到写锁定的机会。这同样是为了避免“迭代读锁定”。因为如果先给 goroutine 2 机会,那 goroutine 1 的写锁定不是还得等吗?要真是这样的话,假如 main goroutine 和 goroutine 1 都在不断地试图读锁定,那么 goroutine 2 就会一直阻塞下去。 所以说,如果这个事情让你的程序停滞了,那么你就要检查一下程序中是不是有“迭代读锁定”的情况。

    2021-05-07
    4
    7
  • 安排
    goroutine和协程有什么本质区别啊,搜了网上也没看出来啥本质区别,有这方面的资料吗?

    作者回复: 传统的协程只是线程内的流程控制工具。它没法做到一个线程内有两个及以上控制流同时进行,只能是这一个挂起那一个运行然后那一个挂起这一个再运行。同时它也不属于多线程编程,没法统一调度多个线程内的控制流。 goroutine 我就不用多说了,它属于用户级线程,与系统级线程搭配使用,很强大也很灵活。深入的东西可以看我的那本《Go 并发编程实战》。

    2019-09-22
    3
    6
  • NoTryNoSuccess
    请问老师,多核心条件下如果两个goroutine底层同时运行在两个线程上,那么此时这两个goroutine实际上是完全并行的。此时它们如果同时进行互斥锁的锁定操作(随后可能同时对同一资源进行写操作)岂不是不能达到对临界区的保护目的了吗?

    作者回复: 互斥锁、条件变量和原子操作都是由操作系统和CPU指令集支撑的,所以Go语言的这些同步工具是可以在多线程以及多核CPU甚至多CPU上正确执行的。无需担心。相反,这些同步工具恰恰针对的就是并发和并行的应用场景。这正是它们的用武之地啊。

    2020-04-04
    3
    4
  • CcczzZ
    老师,有个疑问,文中说的这句:「对读锁进行解锁,只会在没有其他读锁锁定的前提下,唤醒“因试图锁定写锁,而被阻塞的 goroutine”」。 我的理解是,对读锁进行解锁时,此刻若存在其他读锁等待的话,是会优先唤醒读锁的,如果不存在其他等待的读锁,才会唤醒写锁。不知道这样理解是否正确? 而基于上面的理解,我写了段代码测试了一下,发现结果并不是这样,实际情况是:「当读锁进行解锁时,若此刻存在其他的读锁和写锁,会根据他们实际阻塞等待的时间长短,优先唤醒并执行」 就像下面,写锁在前面执行,等待的时间也比读锁场,所以当读锁解锁时,优先唤醒的是等待时间较长的写锁。 func main() { var rwMu sync.RWMutex // 模拟多个写/读锁进行阻塞,当释放读锁的时候看谁先获取到锁(会在没有其他读锁的时候,唤醒写锁) rwMu.RLock() fmt.Println("start RLock") // 写 go func() { defer func() { rwMu.Unlock() fmt.Println("get UnLock") }() rwMu.Lock() fmt.Println("get Lock") }() time.Sleep(time.Millisecond * 200) // 读 go func() { defer func() { rwMu.RUnlock() fmt.Println("get RUnLock") }() rwMu.RLock() fmt.Println("get RLock") }() time.Sleep(time.Millisecond * 200) rwMu.RUnlock() fmt.Println("start RUnLock") time.Sleep(time.Second * 1) } 运行结果(等待时间较长的写操作先执行了): start RLock start RUnLock get Lock get UnLock get RLock get RUnLock

    作者回复: 简单一句话:读写锁中的读锁锁定操作之间是不互斥的。另外,对于读写锁,读锁锁定操作会与写锁锁定操作互斥,写锁锁定操作之间也会互斥。

    2020-01-16
    3
    4
  • 大王叫我来巡山
    需要请教老师的是,主协程收到信号就被唤醒了,认为可以读了,但是被阻塞的写协程收到锁释放的消息会不会比主协程要早,然后继续获得写的机会,主协程会不会被阻塞?我认为是不会的,此处的锁只是保证了不同写协程互斥的写入,也就是写操作是原子的,但是并不保证读操作一定在写完后就读吧

    作者回复: 对于非缓冲通道,写的 goroutine 必然会先完成操作。锁本身只保证互斥。被阻塞的 goroutine 也会有先有后,但会根据被阻塞那一刻的先后,而不是什么读写的先后。 另外互斥锁跟原子操作有本质上的区别,不要搞混。 再另外,goroutine 与协程也有本质上的区别,不要搞混。

    2019-09-13
    2
    3
  • 芝士老爹
    如果一直有新的读锁请求,会不会导致写锁锁不了? 还是说如果有了一个wlock锁请求了,现在因为有rlock未释放锁,wlock的协程被阻塞,后面再有新的rlock锁请求也会先被阻塞,等待wlock锁协程先恢复?

    作者回复: 那要看谁先等待了,这里的等待队列是先进先出的。

    2019-08-04
    3
    3
  • soooldier
    配套代码里puzzlers/article26下并没有demo58.go,也没有demo59.go,懵圈中。。。

    作者回复: 看这里吧:https://github.com/hyper0x/Golang_Puzzlers/tree/master/src/puzzlers/article22 你可以把这里的 article 理解成 topic。一些比较长的 topic 可能会被编辑拆分为多篇文章,所以就出现了这种情况。太长的文章对读者们不太友好,不容易集中精力读下去。 你可以对照着专栏的目录,按照主题,找一下对应的 articleXX 目录。 Update: 我刚刚添加了一个序号映射表:https://github.com/hyper0x/Golang_Puzzlers/blob/master/mapping_table.md 。你用这个就可以方便地对照了。

    2019-05-31
    2
收起评论
显示
设置
留言
49
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部