多线程编程中的一些原则
目录
关于 C++ 多线程编程一的些基本知识可以参考本博客的《C++11/14 新特性(多线程)》 ,《Unix线程基础》。本章不是多线程编程教程,而是个人经验的一些总结。这些经验有一些可能是不正确的,希望在今后的编程中实践、改进。
线程同步的四项基本原则:
- 最低限度地共享对象。对象尽量不要暴露给别的线程,如果需要暴露,优先考虑 immutable对象。否则尽量使用同步措施来充分地保护它
- 尽量使用高级地并发编程构件,如 任务队列、生产者消费者模式等
- 只用非递归的互斥器和条件变量,慎用读写锁,尽量少用信号量
- 除了使用
atomic
整数外,不要自己编写 lock-free 代码,也不要用"内核级"同步原语
互斥器 Mutex
mutex 是最常用的同步原语,它保护一个临界区,任何时候最多只能有一个线程能够访问 mutex 保护的域。使用 mutex 主要是为了保护共享数据。一般原则有:
- 使用
RAII
手法封装 mutex 的创建、销毁、加锁、解锁操作,充分保证锁的有效期等于其作用域,而不会因为中途返回或异常而忘记解锁。这类似于 Java 的synchronized
或 C# 的using
语句。 - 使用非递归的 mutex
- 尽量不要人为地调用
lock()
和unlock()
函数,将这些操作交给栈上的guard
对象,利用其构造与析构函数。 - 不要跨线程地加解锁,避免在不同的函数中分别加锁\解锁,避免在不同的语句分支中加锁\解锁
- 每当构造 guard 对象时,需要考虑栈上已有的锁,防止因加锁顺序不同而导致死锁
- 避免跨进程的 mutex, 进程间通讯尽量使用
TCP sockets
只使用非递归地 mutex
mutex 可分为递归(recursive)
与非递归(non-recursive)
两种,也叫做可重入(reentrant)
与非可重入(non-reentrant)
。它们的区别就是,同一线程可以重复地对 recursive mutex 加锁,而不能重复对 non-recursive mutex 加锁。在同一个线程中多次对 non-recursive mutex 加锁会立刻导致死锁或程序崩溃。
它们的性能相近。其实 recursive mutex 会稍快一些(因为少了一个计数器),且它用进来更方便。也正是因为它方便,所以会隐藏一些问题。一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
std::mutex mu; std::vector<Foo> foos; void post(const Foo& f) { std::lock_guard<std::mutex> locker(mu); foos.push_back(f); } void traverse() { std::lock_guard<std::mutex> locker(mu); for(auto f : foos) { f->do_sth(); } } |
这个程序看起来是正确的。然而,当 do_sth()
函数中间接调用了 post()
函数,程序将会死锁。如果将 mu
对象的类型改为std::recursive_mutex
, 则 可能 导致迭代器失效,进而引发 crash。这个时候只需要打印出线程信息,就可以排查出错误。
要解决间接调用的问题,可以将 post()
分为两个版本,分别为 post()
与 post_without_lock()
:
1 2 3 4 5 6 7 8 9 10 |
void post(const Foo& f) { std::lock_guard<std::mutex> locker(mu); post_without_lock(f); } void post_without_lock(const Foo& f) { foos.push_back(f); } |
条件变量 condition_variable
mutex 是加锁原语,用来排它性地访问共享数据。condition_variable 是等待原语。其含义是一个或多个线程 p 阻塞地等待某个变量 c,一但 c 满足条件,线程 p 将被唤醒。条件变量的正确使用方式:
- 必须与 mutex 配合使用, mutex 用于保护一个布尔表达式 expr
- mutex 加锁后才可以调用
wait()
- expr 的判断和 wait 需要放到
while
循环中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
std::mutex mu; std::condition_variable con; bool response_back; int ver; int get_version(){ std::lock_guard<std::mutex> locker(mu); response_back = false; request(std::bind(on_response, std::placeholder::_1)); while(!response_back) { con.wait(); } return n; } void on_response(int version){ ver = version; response_back = true; con.notify_one(); } void request(std::function<void(int ver)> callbk){ sent_request(callbk); } |
这里不可以使用 if
来替代 while
,因为可能出现 spurious_wakeup
:参考1, 参考2
===待续===