当你要进行线程间同步的时候
你可以定义互斥类
这里面有三个主要的类
一个是mutex
一个是recursive_mutex
一个是timed_mutex
第一种是基本的互斥
它提供了三个核心的成员函数
lock()、try_lock()、unlock()
这个和我们刚才解释是一样的
这里我们就不特别说明了
这三个函数也没有什么原型可介绍的
没有参数 也没有返回值 很简单
递归互斥和定时互斥
和我们刚才的解释也是一样的
这个递归互斥
它就是允许你多次去加锁和解锁
可以避免死锁的问题
定时互斥 它有一个特定时间限制
在特定时间内要定时的
后面两个类同样有这样的成员函数
lock()、try_lock()和unlock()
还有一个叫共享定时互斥
叫shared_timed_mutex
这个只在C++14以后才有
C++11里是没有的
特别注意这一点
我们看具体的实际代码
你可以定义一个互斥类的一个对象x
在我们的线程函数里边
当需要进行同步访问的时候
要锁定这个临界区的时候
简单 在x上面调用x.lock() 锁定它
如果锁定不了 它就会阻塞
它就进不去这个临界区
锁定了它就会进入这个临界区
其它线程就进入不了了
锁嘛 是唯一的嘛
相互独占嘛
当利用完了你就x.unlock() 解锁
它就离开了这个临界区
很方便吧 就是一个缺省构造
x：std::mutexx 缺省构造
用的时候x.lock()
不用的时候x.unlock() 搞定
这就是互斥类的最基本的用法
当然还是我们前面说的
它就是有可能导致死锁的
主程序里边没有什么可说的
我们定义了一个向量
构造了8个线程
这个向量里面存的都是那个线程的地址
不是那个线程本身
因为你要是线程本身的话
这个地方是不能够简单赋值的
如果你要赋值的话那不就拷贝赋值嘛
那是不允许的
你必须是移动赋值才可以
特别注意这一条 其它没什么
就是我刚才讲的
你互斥最主要的问题
是容易导致死锁的呀对吧
如果一个特定的线程在临界区操作
导致了异常
这个锁就没有办法释放嘛
然后其它线程
想获取这个锁的线程
就被永久地阻塞了
这是第一种情况
临界区的代码里面有多路分支
那么有一些分支里边你忘了解锁
那完蛋了 它肯定也不成 对吧
有一些分支提前结束了
没有实行解锁的行为
那么这个系统肯定也有问题
想进入临界区的其它的线程
肯定也会被永久阻塞
如果多个线程同时申请这个资源的时候
加锁的次数 不一样
也可能到这个问题
两个线程同时想访问两个资源
一个拿了A 一个拿到了B
第一个线程需要第二个线程的资源
第二个线程需要第一个线程的资源
结果俩人就在那个地方僵持着了
那不就死锁了嘛
这种情况也是需要特别注意的
所以正常情况下边
你编程序的时候要特别注意这一点
就是当多个线程
同时需要多个资源的时候
那个加锁的次序一般来讲
应该保持它的一致性
如果不能够保持这一点
就需要特别注意死锁的问题
那么我们怎么避免这个死锁呢
在C++11里边有一个最基本的设计原则
所谓资源获取即初始化
所谓的RAII
叫资源获取就是初始化
就这个意思
所以在实现它的时候
我们就可以使用互斥对象管理类模板
来管理我们这样的资源
就是我们不直接使用这个互斥
那个互斥对象我们当然要定义
但是我们不直接使用那个互斥对象
我们使用互斥对象管理类的模板
构造那样的对象
来管理我们的互斥对象
讲起来很拗口
待会看例子你就知道了
在C++11里边提供了几个
锁的管理类模板
一个是lock_guard
这是一个简单的
基于作用域的锁管理类模板
构造这个lock_guard的时候
它对应的那个互斥锁是不是加锁的
是可选的 你可以选择
如果你不加锁
它就假定当前线程
已经获得了这个锁的所有权
随时都可以把它加锁
析构的时候呢它是自动解锁的
你只管lock就OK了
你不用管unlock
实际上如果你构造的时候就加锁
你连lock都不用管
它自动就帮你锁上
为什么叫基于作用域的锁管理类模板呢
就是你定义了这个对象
在它离开它的作用域以后
这个对象就会被释放
当这个对象被释放的时候
那个锁自动地就会解开
所以解锁这个动作
大部分的时候你就不用关心了
这是基于作用域的
锁管理类模板 lock_guard
第二种就是独一锁的管理类模板
叫unique_lock
构造的时候是不是加锁依然是可选的
对象析构的时候
如果持有这个锁
它就会自动地解锁
但是它有一个特别的地方就是
它为什么叫独一锁呢
就是这个所有权可以转移
对象的生存期里
允许你多次的手工地调用
加锁和解锁的这个动作
你可以去做这个事情
lock_guard你不用管
lock或unlock那个事
它自动替你处理这个问题
大部分时候是这样
这个unique_lock呢
你可以手工地去决定什么时候加锁
什么时候解锁
还有一个叫共享锁的管理类模板
那是C++14以后才有的叫shared_lock
有三个互斥管理策略
第一个std::defer_lock
它就是在构造我们互斥管理对象的时候
延迟这个加锁操作
不立即把它锁定
第二个是try_to_lock
尝试着去锁定
锁定不成功的时候
它就不阻塞这个线程
互斥体如果不可用
它就直接就返回了 不会锁定的
第三个就是adopt_lock
它什么意思呢
就是假定当前这个线程
已经获得了这个互斥体的所有权
它不再加锁
它不尝试加锁这个动作
就是假定这个锁你已经拿到了
加锁那是后来的事
在使用这个互斥管理策略的时候要注意
如果你没有提供它的互斥管理策略
那么缺省的情况下
它在构造这个互斥管理对象的时候
就会阻塞当前的线程
直到成功地获取对应的互斥
有一点是需要特别注意的
这个互斥什么时候解锁是恰当的
虽然C++11
提供了这个互斥自动管理策略
看上去很方便
让我们在编程的时候不用太关心
什么时候加锁
什么时候解锁这个问题
但实际上事情没那么简单
如果你在一个作用域内
定义了一个互斥管理对象
加锁了
临界区完了
但是作用域还没有结束
那个互斥就没有被解锁呀
其它的线程都会被阻塞了
所以特别注意这一条
所以在这种情况下边
临界区的代码本身就不应该太长
可是如果你忘了
在这个临界区代码结束之后
立即把这个锁给解开
而这个互斥对象作用域还没有结束
那么这个锁就一直处于锁定状态
其它的线程就不能够用
所以这个时候要特别注意这一点
典型的解决方案是什么呢
就是你使用一个复合语句结构
可以把这个临界区这段代码
就用复合语句块把它括起来
在语句块开头加这个锁
在语句块的结尾
临界区做完了嘛
那个对象自动地就会被析构
锁就会被打开
当多个资源需要竞争访问的时候
有好几个互斥需要竞争访问
那么这个时候就需要特别注意
为了避免死锁
你可以使用标准库的std::lock()、std::try_lock()
去尝试去锁定它
标准库里边的这两个函数lock()和try_lock()
能够预先发现死锁的存在
并且避免它
讲起来会觉得很奇怪
不知道该怎么用
我们看具体的例子 你就明白了
这个例子我们会使用互斥管理策略类
来重现实现我们原来的线程函数
这是我们的劳工类
在这个线程函数内部
我使用一个复合的花括号对
你看它顶头没有if 也没有switch
也没有for、while 什么东西都没有
就是一个孤零零的花括号对
就构造一个语句块
在这个语句块的开头
我定义一个std::lock_guard〈std::mutex〉
这个类的一个对象 locker
传x 那个互斥进去
就是锁定一个互斥
因为它是一个互斥管理类嘛
所以它的模板
那个参数必须是一个互斥
构造它的时候就是locker(x)
传一个互斥进去
这个locker这个对象就被我们构造好了
缺省的时候一构造完毕
它就把它加锁
如果加不上
这个函数 这个线程就会被阻塞
函数被阻塞了
那么就做不下去了
所以这段代码一过去
锁一定是拿到了
临界区代码执行就完了
花括号对结束
这个作用域就结束了
locker就会被释放
不用等到输出完毕之后
才销毁那个locker
才释放那个锁 不需要
在这个位置就销毁了 多方便啊
这就是我们的lock_guard
这个管理类模板使用方法
特别简单吧
我们看第二个例子
就是我们刚才转账处理的
那个程序的一个扩展
我们不使用刚才那段代码了
没有那个double数组
我们定义Account类
这样的一个账户类
在这个账户类里面
有两个私有成员
一个是_balance 表示它的余额
一个是_x 就是访问这个对象的时候的
对应的互斥
有几个成员函数 GetBalance()
Increase()、Decrease() 给它加钱、减钱
还有一个函数GetMutex()
得到它的互斥
如果我有一个函数叫Transfer()
我们会传两个账户对象的引用进去
这是我们的Transfer()
这个线程函数
我们会做锁定动作
这是两个账户要传递钱啊 要转账啊
所以我们要锁定两个账户
一个账户要加
一个账户要减嘛 对吧
我们要锁定两个账户
我们用unique_lock锁定from账户
它的对应的互斥
这是locker1
第一个参数传from.GetMutex()
它的策略是adopt_lock
第二个是unique_lock〈std::mutex〉
locker2 to.GetMutex()
获取to的那个账户的互斥
锁定策略也一样
接下来我们锁定它
std::lock() 调用这个全局函数
定义在标准名空间里边的这个函数
锁定它 from.GetMutex(),to.GetMutex()
同时锁定这两个互斥 然后转账
函数一结束 作用域就消失了
locker1、locker2解锁
这两个量就会被释放嘛
它就解锁了 这就很典型
我们在这里构造两个线程对象t1和t2
注意我们要传线程函数Transfer
但是这个Transfer呢
这个线程函数是需要带三个参数的
那我们就需要把这三个参数传进去
但是你要注意
线程函数本身
是在C语言格式里面就有的
所以它只有值传递一种
如果在C++代码里
想要向一个线程函数传递一个引用
你必须明确的用std::ref()
取到这个对象的引用
才能传进去
std::ref(a1)、std::ref(a2)传进去
传转账的数额OK
t2线程也一样
然后t1.join()、t2.join()
等这两个线程做完
你多运行几遍
你看一下你就知道
有的时候t1线程会先做
有的时候t2线程会先做
但不管怎么样
每次操作的时候
转账的这个临界区内部代码
要么不做 要么做完