一、锁的根本目标:维持不变式
类一般都维持一些不变式,就是关于一个数据结构的总是为真的陈述(好像一般都是指内部变量的意义)。比如链表的两个不变式是,一个节点的next指针总是指向下一个节点,链表的number成员变量总是指明了节点总数。这些不变式总会在链表变动时被破坏,比如向链表插入一个节点时,总会有一个时刻,某个节点的next指针指向的不是它真正的下一个节点,number成员也会有一个时刻表达的不是节点真正的总数。如果在这些时刻有其它线程来操作这些不变式被破坏的地方,那就会引发竞争条件(最终的结果依赖于两个线程的执行次序)。所以锁的存在就是为了对外部保持不变式,不让外部接触到不变式暂时被破坏的地方。
二、单个锁的用法和注意事项
一个锁一般都是为了保护一份共享数据,只要有一个操作会破坏不变式,那么所有对这个数据的访问都要锁上这个锁,以防接触到被破坏的状态。由于这个锁和数据联系紧密,一般会封装在一个类里。在使用时,一般在成员方法开头用 std::lock_guard 或者 std::unique_lock 锁起来,而不是直接裸调 std::mutex::lock ,因为前两者可以使用RAII技术恰当地解锁。而且即使这个锁之前已经锁上了,或者稍后才要上锁,也最好将锁委托给 std::lock_guard 和 std::unique_lock 来解锁。
一个特别值得注意的问题是,千万不要把共享数据的指针或者引用传到锁的范围之外,比如用成员方法的参数或者返回值把它传到外面去,那显然数据就不再受锁的保护了。其它的危险方式还有:把共享数据的指针或者引用,存到外部可见的内存区,或者传递到用户提供的回调函数中(因为回调函数可能会把这个指针或引用存起来)。书中斜体字原文为:“Don‘t pass pointers and references to protected data outside the scope of the lock, whether by returning them from a function, storing them in externally visible memory, or passing them as arguments to user-supplied functions.”
即使这样,还是有潜在的危险:锁一般只能保证一个成员方法内部的正确性,如果两个成员方法之间有逻辑上的依赖关系,那锁就无能为力了。比如仅当 std::stack::empty() 返回 false 时,才能调用 std::stack::pop() ,虽然这两个方法内部都正确加了锁,但如果这个两个调用之间插入了另一个线程的 std::stack::pop(),显然就可能出问题。至于 std::stack 为什么这么设计,以及如何解决这个问题,因为有些复杂,这里不细说,详见书中讲述。这里简单总结解决方法:1.在外面构造好存放结果的变量,作为引用或指针传给 pop(result);2.pop() 返回shared_ptr。
三、多个锁引起的问题:死锁
死锁的四个必要条件:互斥(一个资源每次只能被一个线程使用)、请求和保持、不剥夺、环路等待。
C++ 11提供的 std::lock() 可以一次给多个锁上锁,方便多了,但是需要同时拿到所有锁的对象(或指针引用)才可以,如果锁分散在多个模块中(比如用户回调函数中)就比较麻烦了。避免的方法有:
1. 持有一个锁时就不再请求另一个锁;
2. 持有锁时避免调用回调函数;
3.
如果一定要调回调函数的话,那就安排一个固定的锁次序;
4. 设计锁层次