读陈硕先生的《moduo多线程服务器编程》第二章线程同步精要,做下笔记。
编程概要
- 首要原则是尽量最低限度共享对象,减少需要同步的场合。
- 其次是使用高级的并发编程组件,如TaskQueue、Producer-Consumer Queue、CountDownLatch等. [链接](https://github.com/chenshuo/muduo)
- 最后不得已必须使用底层同步原语时,只用非递归的互斥器和条件变量,慎用读写锁,不要用信号量。
- 除了使用atomic整数之外,不自己写lock-free代码,也不要用“内核级”同步原语。
互斥器(mutex)
互斥器用于保护临界区,任何一个时刻最多只能有一个线程在此mutex划出的临界区内活动,单独使用mutex时,主要是为了保护共享资源。编程原则:
1. 用RAII手法封装mutex创建,销毁,加锁,解锁这四个操作。具体代码见最后。
2. 只用非递归的mutex(不可重入的mutex)。因为少用一个计数器,比递归的mutex略快一点,但主要还是为了设计意图,不是为了性能考虑。在同一个线程中多次对非递归mutex加入会立刻死锁,能帮助我们及早发现问题。
3. 不手工调用lock()和unlock()函数,一切交给栈上的Guard对象构造函数和析构函数负责。
4. 每次构造Guard对象,思考一路上(调用栈上)已经持有的锁,防止因加锁顺序不同而导致死锁。
次要原则:
1. 不使用跨进程的mutex,进程间通信只用TCP sockets。
2. 加锁、解锁都在同一个线程,线程a不能去unlock线程b已经锁住的mutex。
3. RAII保证解锁与不重复解锁。
4. 必要的时候可以考虑PTHREAD_MUTEX_ERRORCHECK来排错。
note: Linux的Pthreads mutex 采用 futex 实现,不必每次加锁、解锁都陷入内核。
条件变量(cond)
如果需要等待某个条件成立,应该使用条件变量,学名管程。
对于wait端:
1. 必须和mutex一起使用,该布尔表达式的读写需受此mutex保护。
2. 在mutex已上锁的时候才能调用wait()。
3. 把判断布尔条件和wait()放到while循环中。
例子1:
链接
例子2:
muduo::MutexLock mutex;
muduo::Condition cond(mutex);
std::deque<int> queue;
int dequeue(){
MutexLockGuard lock(mutex);
while(queue.empty()){
cond.wait();
}
int top = queue.front();
queue.pop_front();
return pop;
}
note:必须使用while循环来等待条件变量,不是使用if语句,原因是spurious wakeup。
对于signal/broadcast端:
1. 不一定要在mutex已上锁的情况下调用signal
2. 在signal之前一般要修改布尔表达式。
3. 修改布尔表达式通常要用mutex保护。
4. broadcast通常用于表明状态变化,signal通常用于表示资源可用。
例子1:
链接
例子2:
void enqueue(int x){
MutexLockGuard lock(mutex);
queue.push_back(x);
cond.notify();
}
note:互斥器和条件变量构成了多线程编程的全部必备同步源于,用它们即可完成任何多线程同步任务,二者不可相互替代。
不用读写锁和信号量
- 正确性上:易发生在持有read lock时候修改了共享数据,这种错误的后果跟无保护并发读写共享数据时一样的。
- 性能上:读写锁不见得比普通mutex更高效,如果临界区很小,锁竞争不激烈,mutex往往更快。
- 通常reader lock是可重入的,writer lock是不可重入的。但为了防止writer饥饿,writer通常会阻塞后来的reader lock,因此reader lock在重入的时候可能死锁。
不用信号量:
1. 使用条件变量配合互斥器可以完全替代信号量,而且不易出错。
2. 信号量增加了程序设计的负担和出错的可能。
代码
MutexLock:
链接
MutextLockGuard:
链接
Condition:
链接