文章目录
线程同步的四项原则, 按重要性排列:
-
首要原则是尽量最低限度地共享对象, 减少需要同步的场合。一个对象能不暴露给别的线程就不要暴露如果暴露就优先考虑immutable对象(const);实在不行才暴露可修改的对象,并用同步措施来充分保护它。
-
其次, 使用高级的并发编程构件, 如任务队列(TaskQueue), 生产者消费者队列(Producer-Consumer Queue), 倒计时(CountDownLatch)等。
-
最后不得已才使用底层同步原语(primitives)时, 只用非递归的互斥器和条件变量, 慎用读写锁, 不要用信号量。
-
除了使用atomic整数之外, 不要自己编写lock-free代码, 也不要用"内核级"同步原语; 不能凭空猜测’那种做法性能会更好’, 比如spin lock(自旋锁) vs mutex(互斥量);
一、互斥量(mutex)
mutex是最常用的同步原语。mutex可以保护一个临界区,使得任何时刻最多仅有一个线程在临界区内活动。
1、互斥量的使用原则
-
RAII封装mutex的生存及其相关操作。
-
使用Guard管理mutex锁,并利用生存期控制Guard,基于此结构(Scoped Locking)可以使debug很方便。
-
构造Guard时,防止加锁顺序不同导致死锁。
-
使用非递归的mutex。
-
跨进程时使用tcp sockets。
-
尽量使用高层同步方法。
2、只使用非递归的mutex
mutex分为递归(recursive)和非递归(non-recursive)两种, 这是POSIX的叫法, 另外的名字是可重入(reentrant)和非可重入。这两种mutex对线程间(inter-thread)的同步基本没有区别, 他们的唯一区别就是: 同一线程可以重复对recursive mutex加锁, 但是不能重复对non-recursive mutex加锁。
recursive mutex(可重入的互斥量)可能会隐藏代码里的一些问题:典型情况是, 以为拿到一把锁就可以修改对象了, 没想到外层代码已经拿到了锁, 正在修改(或读取)同一个对象。
3、死锁
坚持使用Scoped Locking(作用域加锁), 很容易在出现死锁的时候定位bug。
二、条件变量(condition variable)
互斥器(mutex), 是加锁原语, 用来排他性地访问共享数据, 它不是等待原语;如果需要等待某个条件成立, 应该使用条件变量(condition variable);条件变量是一个或多线程等待某个布尔表达式为真, 即等待别的线程唤醒它。
【wait 端】:
-
与mutex一起使用;
-
在mutex上锁时才能调用;
-
把判断bool条件放到while循环中(避免虚假唤醒(spurious wakeup))。
【signal/broadcast端】:
-
不一定要在mutex已上锁时调用signal;
-
在signal之前一般要修改bool表达式;
-
修改bool表达式时通常要mutex保护;
-
注意区分signal与broadcast。
// Wait 端一个代码示例
muduo::MutexLock mutex;
muduo::Condition cond(mutex);
std::deque queue;
int deque()
{
//与mutex一起使用,上锁时才能调用
MutexLockGuard lock(mutex);
// "queue.empty()" 是这里的bool表达式
while(queue.empty())
{
cond.wait();//并放到while循环中
//唤醒后可能仍不满足条件所以用while
}
assert(!queue.empty());
int top=queue.front();
queue.pop_front();
return top;
}
void enqueue(int x)
{
MutexLockGuard lock(mutex);//修改表达式时通常要mutex保护
queue.push_back(x);
//此处已经将bool表达式修改完毕,可以发送signal
cond.notify();//发送signal,理论上此处调用时不需要mutex上锁,可以移出临界区之外
//notify只通知一个,且随机决定;notifyAll通知所有。
}
condition variable 是十分底层的同步原语,可以用它实现高层的一些同步措施。如倒计时(CountDownLatch), 线程池(ThreadPool),队列等。mutex和condition variable不能互相替代,它们共同构成了多线程编程的全部必备同步原语。
三、读写锁、信号量
读写锁(Readers-Writer lock, 简写为rwlock)是个看上去很美的抽象, 它明确区分了read和write两种行为。但不见得比mutex更高效。
四、sleep(3)不能保证同步
一般用于构建一些测试情况时使用,sleep本身只负责延时,一般只能出现在测试代码中。否则程序的设计一般是有问题的。
五、总结
线程同步的四项原则:
-
最低限度共享对象,减少需要同步的场合。
-
使用高级的并发编程构件。
-
若不得已使用底层同步原语,只是用 non-recursive mutex 和 condition variable,其他如读写锁、信号量最好不要用。使用时利用RAII和Scoped Locking保证结构清晰。
-
除了使用atomic整数意外不去自己编写lock-free代码,也不要用内核级同步原语。
一般依照这些原则就可以应付多线程开发的各种场合了。"让一个正确的程序变快"远比"让一个快的程序变正确"容易得多。所以首先保证程序的清晰、简单和正确性,再去考虑性能优化。