2.1 mutex
2.1.1 只使用非递归的 mutex
递归锁(recursive mutex)可能会隐藏一些代码中的问题:典型情况是以为拿到一个锁就能修改对象,没想到外层代码已经拿到了锁,正在修改(读取)同一个对象。
如果Foo::doit()
间接调用了post()
,就会出现问题:
mutex
是非递归的,于是死锁了;mutex
是递归的,由于**push_back()**
潜在导致迭代器失效的可能(vector扩容),程序偶尔会crash掉。
如果确实需要在便利的时候修改vector
,有两种做法:
- 把修改推后,先记住循环中试图改动哪些元素,等循环结束再根据记录进行改动
- 使用 copy-on-write,或者更准确的说:copy-on-other-reading。
2.8 借助 shared_ptr 实现 copy-on-write
由于 2.8 小节和 2.1.1 小节的内容关联较大,所以提前拿过来写一下
使用shared_ptr
管理共享数据:
首先修改数据结构:
在 read 端,用一个栈上局部FooListPtr
变量当做观察者,它使得g_foos
的引用计数增加,临界区内只读取了一次共享变量g_foos
,而且多个线程调用traverse()
也不会相互阻塞:
对于 write 端,如果g_foos.unique()
为 true,就可以原地修改FooList
;如果为 false,说明别的线程正在读取FooList
,所以不能原地修改,而是要拷贝一份,在副本上修改(参照shared_ptr
的线程安全性),这样避免了死锁:
如果一个函数可能在加锁的情况下被调用,也可能在未加锁的情况下被调用,那么可以拆成两个函数:
- 跟原来函数同名,函数加锁,调用第二个函数;
- 给函数名加上“WithLockHold”后缀,不加锁,把原来的函数体搬过来。
2.1.2 死锁
有一个Inventory
类记录当前Request
对象:Request
类与Inventory
交互逻辑很简单,在处理(process)请求时,往g_inventory
中添加自己;在析构时,从g_inventory
中移除自己:
下面这个程序运行起来发生了死锁:
通过 gdb 查看两个线程的调用栈,发现等在mutex
上,应该是发生了死锁。因为一个程序中的线程一般只会等在condition_variable
或epoll_wait
上:
可以看到:
解决可以参考 2.8 小节中的:
2.8 将 print 移出 printAll 临界区
将request_
复制一份,在临界区外遍历这个副本:
但是复制了整个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
替换读写锁。(实现略)