2.1 mutex

2.1.1 只使用非递归的 mutex

递归锁(recursive mutex)可能会隐藏一些代码中的问题:典型情况是以为拿到一个锁就能修改对象,没想到外层代码已经拿到了锁,正在修改(读取)同一个对象。
image.png
如果Foo::doit()间接调用了post(),就会出现问题:

  • mutex是非递归的,于是死锁了;
  • mutex是递归的,由于**push_back()**潜在导致迭代器失效的可能(vector扩容),程序偶尔会crash掉。

如果确实需要在便利的时候修改vector,有两种做法:

  1. 把修改推后,先记住循环中试图改动哪些元素,等循环结束再根据记录进行改动
  2. 使用 copy-on-write,或者更准确的说:copy-on-other-reading。

    2.8 借助 shared_ptr 实现 copy-on-write

    由于 2.8 小节和 2.1.1 小节的内容关联较大,所以提前拿过来写一下

使用shared_ptr管理共享数据:
image.png
首先修改数据结构:
image.png

在 read 端,用一个栈上局部FooListPtr变量当做观察者,它使得g_foos的引用计数增加,临界区内只读取了一次共享变量g_foos,而且多个线程调用traverse()也不会相互阻塞:
image.png
对于 write 端,如果g_foos.unique()为 true,就可以原地修改FooList;如果为 false,说明别的线程正在读取FooList,所以不能原地修改,而是要拷贝一份,在副本上修改(参照shared_ptr的线程安全性),这样避免了死锁:
image.png

如果一个函数可能在加锁的情况下被调用,也可能在未加锁的情况下被调用,那么可以拆成两个函数:

  1. 跟原来函数同名,函数加锁,调用第二个函数;
  2. 给函数名加上“WithLockHold”后缀,不加锁,把原来的函数体搬过来。

image.png
在误用情况下会有问题。

2.1.2 死锁

有一个Inventory类记录当前Request对象:
image.png
image.png
Request类与Inventory交互逻辑很简单,在处理(process)请求时,往g_inventory中添加自己;在析构时,从g_inventory中移除自己:
image.png
下面这个程序运行起来发生了死锁:
image.png
通过 gdb 查看两个线程的调用栈,发现等在mutex上,应该是发生了死锁。因为一个程序中的线程一般只会等在condition_variableepoll_wait上:
image.png
可以看到:
image.png
解决可以参考 2.8 小节中的:

2.8 将 print 移出 printAll 临界区

request_复制一份,在临界区外遍历这个副本:
image.png
但是复制了整个std::set的每个元素,开销较大;如果遍历期间没有其他人修改requests_,可以减小开销,这就引出了第二种做法:
使用**shared_ptr**管理**std::set**,在遍历时先增加引用计数,防止并发修改。方案等同于之前post()traverse()。(用一个小作用域加锁拷贝一下,增加引用计数)

2.3 不要用读写锁和信号量

通常 reader lock 是可重入的,writer lock 是不可重入的。但是为了防止 writer 饥饿,writer lock 通常会阻塞后来的 reader lock,因此 reader lock 在重入时就可能死锁。另外。在追求低延迟读取的场合也不实用读写锁

可以使用std::shared_ptr和普通mutex替换读写锁。(实现略)