golang并发编程-02多线程编程-02线程的同步
@
目录
1. 互斥量
1.1 互斥
在同一时刻,只允许一个线程处于临界区之内的约束被称为互斥(mutex)。
1.2 互斥量
每一个线程在进入临界区之前都必须先锁定某个对象,这个对象被称为互斥对象或互斥量。
只有成功锁定对象的线程才会被允许进入到临界区之内,否则就会被阻塞。
1.2.1 互斥量的使用
- 互斥示意图
- A、B两个线程,使用α、β两个互斥量的示意图
1.2.2 互斥量使用原则
- 尽量少地使用互斥量
- 每个互斥量保护的临界区(在合理范围内)尽量地大
- 避免不同互斥量保护同一临界区
- 临界区切分,并使用不同互斥量保护。
切分的条件:
- 发现多个线程会频繁地出入某个较大的临界区
- 且它们之间经常存在访问冲突
1.2.3 多个互斥量避免死锁
1)试锁定和退回
访问顺序不确定时使用该方法
- 锁定互斥量A
- 试锁定互斥量B
- 锁定互斥量B
失败-----> 退回并解锁互斥量A
成功 -----> 继续锁定下一个互斥量
2)固定顺序锁定
互斥量有相同的锁定顺序。(如:A---->B---->C---->……)
- 锁定B的前提条件是锁定A
- 那么如果A被线程甲锁定,那么B一定不会被甲之外的其他线程锁定。
2. 条件变量
作用:共享数据的状态发生变化时,通知其他因此而被阻塞的线程。
2.1 生产者-消费者示例
- 题目:
一个容量有限的数据块队列和若干个会操作该队列的线程。
其中的一些生产者线程会生产数据块并把它们添加到数据块队列中
而另一些线程会消费数据块并把它们从数据块队列中删除。
- 异常一
【现象】
生产者锁定互斥量,发现队列已满。
【解决】
锁定互斥量之后,发现队列已满将解锁互斥量,计入队列。
- 异常二
【现象】
消费者锁定互斥量,发现队列为空
【解决】
消费者发现互斥量为空,解锁互斥量,进入队列
2.2 条件变量的使用
使用规则
- 使用一个条件变量之前必须创建和初始化它
- 条件变量的初始化必须要保证唯一性
- 条件变量在被真正使用之前还必须要与某个互斥量进行绑定
对条件变量的操作
- 等待通知(wait)
即阻塞当前线程,直至收到该条件变量发来的通知。
- 单发通知(signal)
让该条件变量向至少一个正在等待它的通知的线程发送通知,以表示某个共享数据的状态已经改变。
- 广播通知(broadcast)
让条件变量给正在等待它的通知的所有线程都发送通知,以表示某个共享数据的状态已经改变。
单发通知示例
- 生产者通知的使用
上图说明:
- 条件变量两个
被用来关注队列的非满状态的条件变量称为条件变量Ⅰ
被用来关注队列的非空状态的条件变量称为条件变量Ⅱ- 两个条件变量绑定同一个互斥量
- 消费者通知的使用
3. 线程安全性
1)重入函数
- 释义:多个线程并发的调用该函数与它们以任意顺序依次地调用它所产生的效果总是相同的,那么该函数就是一个可重入函数。
举例:
- 任何内含了对共享数据进行操作的代码的函数都可以被视为不可重入的函数。
- 如果一个函数把共享数据的值作为其返回的结果或者包含于其返回的结果中,那么该函数就肯定不是一个可重入函数
- 意义:
使函数成为可重入函数是实现其线程安全性的最直接的一种方式,并且也是最简单和最高效的方式。
2)不能使用重入函数的情况
- 应该尽量编写可重入的函数。
- 不能使用重用函数时,使用互斥量把相关的代码保护起来。
说明:
- 把其中的所有代码都置于临界区之中是可行的,但是也是最低效的。
- 应该从函数体中查找出操作共享数据的代码并用互斥量把它们保护起来。
- 还可以把这些代码从函数体中分离出来。