译者:yurunsun@gmail.com新浪微博@孙雨润新浪博客CSDN博客日期:2012年11月14日
原作:Scott Meyers
这些是Scott Meyers培训教程《新C++标准:C++0x教程》的官方笔记,培训课程的描述请见 http://www.aristeia.com/C++0x.html,版权信息请见 http://aristeia.com/Licensing/licensing.html.
漏洞和建议请发送邮件到 smeyers@aristeia.com. 翻译错误请发送邮件给yurunsun@gmail.com (译者注).
9. 并行编程
基本组件:
- 线程作为独立运行单元
std::async
和futures
执行异步调用mutex
用来控制共享数据- 条件变量用于“阻塞直到为true”(类似自旋锁)
- 线程局部变量用于线程内专用数据
相应头文件:
#include <thread> #include <mutex> #include <condition_variable> #include <future>
9.1 线程
std::thread
将能调用的对象作为参数,并异步执行:
void doThis(); class Widget { public: void operator()() const; void normalize(long double, int, std::vector<float>); … }; std::thread t1(doThis); // run function asynch. Widget w; … std::thread t2(w); // “run” function object asynch.
lambda
可以用作可调用对象:
long double ld; int x; std::thread t3([=]{ w.normalize(ld, x, { 1, 2, 3 }); }); // “run” closure asynch.
9.2 数据生存期的考虑
在单线程中的函数调用时,外部数据是冻结状态,意思是:
- 调用过程中这些数据不会被销毁
- 只有函数内能改变这些数据的值
例如:
int x, y, z; Widget *pw; … f(x, y); // call in ST system
调用过程中: x, y, z, pw
都会存在,对于pw
,除非函数内将其销毁;他们的值只能在函数内被改变。
但是异步调用无法保证这种冻结状态:
int x, y, z; Widget *pw; … call f(x, y) asynchronously (i.e., on a new thread);
调用过程中:x, y, z, pw
可能超过生存期,pw
可能被销毁;他们的值可能被改变。通过以传值方式传递参数,可以使函数内部对参数的访问不受影响:
void f(int xParam); // function to call asynchronously { int x; … std::thread t1([&]{ f(x); }); // risky! closure holds a ref to x std::thread t2([=]{ f(x); }); // okay, closure holds a copy of x … } // x destroyed
9.3 创建线程时的参数传递
9.3.1 std::thread
构造函数
std::thread
有一个不定参数的构造函数,将任何参数拷贝一次。
void f(int xVal, const Widget& wVal); int x; Widget w; … std::thread t(f, x, w); // invoke copy of f on copies of x, w
上例中会copy f, x, w
,确保异步回调时变量存在。在函数f
内部,xVal
和wVal
是w
的拷贝的引用,而不是w
的引用。
9.3.2 闭包中传值:
void f(int xVal, const Widget& wVal); int x; Widget w; … std::thread t([=]{ f(x, w); }); // invoke copy of f on copies of x, w
上例中闭包copy了x, w
,然后std::thread
的构造函数会将闭包copy一份,确保异步调用时仍然存在,在f
内部,wVal
是w
的拷贝的引用,而不是w
的引用。
9.3.3 std::bind
void f(int xVal, const Widget& wVal); int x; Widget w; … std::thread t(std::bind(f, x, w)); // invoke f with copies of x, w
上例bind
返回的对象包含x, w
的拷贝,std::thread
会拷贝这个对象,确保异步调用时仍然存在,在f
内部,wVal
是w
的拷贝的引用,而不是w
的引用。
lambda
表达式推荐与bind
结合使用,不仅易于理解,而且效率更高。
9.3.4 确保变量的生存期
一种方法是延后局部变量的销毁时间,直到异步调用结束:
void f(int xVal, const Widget& wVal); // as before { int x; Widget w; … std::thread t([&]{ f(x, w); }); // wVal really refers to w … t.join(); // destroy w only after t finishes }
注意这里会阻塞调用f
的线程
9.3.5 混用传值与传引用
void f(int xVal, int yVal, int zVal, Widget& wVal);
如果真的想传wVal
的引用,而不是拷贝的引用,该怎么做呢?
-
lambda
闭包{ Widget w; int x, y, z; … std::thread t([=, &w]{ f(x, y, z, w); }); // pass copies of x, y, z; pass w by reference }
-
std::bind
和std::thread
构造函数,使用std::ref
void f(int xVal, int yVal, int zVal, Widget& wVal); // as before { static Widget w; int x, y, z; … std::thread t1(f, x, y, z, std::ref(w)); // pass copies of std::thread t2(std::bind(f, x, y, z, std::ref(w))); // x, y, z; pass w by reference }
还存在一个std::cref
表示const引用
9.4 异步调用 std:async
+ std::future
同裸调std::thread
相比,std::async
+ std::future
的方式能够获取异常与返回值,另外调用方式与普通函数更类似。注意与async
相关的概念还有promise
和packaged_task
,这里暂时没有设计。
9.4.1 async
执行异步调用, future
取回结果或者错误
-
调用方式
double bestValue(int x, int y); // something callable and time-costing std::future<double> f = // run λ asynch.; std::async( []{ return bestValue(10, 20); } ); // get future for it do other work … double val = f.get(); // get result (or exception) from λ
-
std::async
可能会使用线程池来实现
9.4.2 async
加载策略
-
std::launch::async
: 在新线程执行 -
std::launch::deferred
: 在get
或waiting
的时候执行auto f = std::async(std::launch::deferred, []{ return bestValue(10, 20); }); … auto val = f.get(); // run λ synchronously here
2010年11月之前
std::launch::deferred
曾叫做std::launch::sync
-
对于deferred方式的async, 根据N2973的lazy evaluation法则,任务会在调用
get/wait
时执行。如果调用wait_for/wait_until
,会立即返回std::future_status_deferred
9.4.3 futures
-
std::future<T>
: 结果可能只能获取一次,可以move但不能copy -
std::shared_future<T>
: 结果可以多次获取,当多个线程需要同一个future
的结果时比较合适,可以move/copy, 可以通过std::future<T>
创建,但是这会导致所有权被剥夺。 -
std::async
和std::promise
都返回std::future
,在2009年11月之前std::future
叫做std::unique_future
。 -
结果通过
get
获取,调用时可能会发生阻塞,然后grab结果。对于future
,grab的意思是:如果能move则move,否则copy;对于shared_future<T>
,grab的意思是获取对方的引用;结果可能是一个异常。 -
对
std::future
的第二次之后的get会导致undefined
的结果,而对std::shared_future
会得到相同的结果。
9.4.4 wait
wait
会阻塞,知道有结果返回。
std::future<double> f = std::async([]{ return bestValue(10, 20); }); … f.wait();
注意可能会超时,必须设置超时时间。
与std::launch::async
方式匹配使用是比较常见的方式:
std::future<double> f = std::async(std::launch::async, []{ return bestValue(10, 20); }); … while (f.wait_for(std::chrono::seconds(0)) != std::future_status::ready) { // if result of λ isn’t ready, do more work ... } double val = f.get(); // grab result
对于unshared futures, 当你知道任务以异步方式运行的时候wait_for
最有用,因为调用wait_for
不会超时,而仅仅是同步执行,一直阻塞至任务完成。。还要注意的是这里的wait_for
不支持等待多个future
中的一个,而windows下的WaitForMultipleObjects
能做到这一点。
9.4.5 void futures
当调用者对返回值不感兴趣(但可能对异常感兴趣)的时候。
void initDataStructs(int defValue); void initGUI(); std::future<void> f1 = std::async( []{ initDataStructs(-1); } ); std::future<void> f2 = std::async( []{ initGUI(); } ); … // init everything else f1.get(); // wait for asynch. inits. to f2.get(); // finish (and get exceptions,if any) // proceed with the program
对于void futures
使用wait还是
get,取决于是否需要超时处理(只有
wait支持超时)以及是否需要获取异常(
get可以做到)。
wait还可以被当成一个信号机制,也就是用来告诉其他线程,他们等待的一个操作已经完成了。此外,
wait可以在任何时候强制执行以
deferred`加载的一步函数。
注意:截止到gcc4.8.1, 仍然存在对std::thread支持的bug, 需要按照如下方式构建qmake. 对Makefile以此类推:
QMAKE_CXXFLAGS += -std=c++0x QMAKE_LFLAGS += -Wl,--no-as-needed -pthread SOURCES += main.cpp
9.4.6 代码示例
using namespace std; int main(int argc, char* argv[]) { cout << "[Main Thread ID] " << this_thread::get_id() << endl; vector<future<void>> futures; for (size_t i = 0; i < 10; ++i) { auto fut = async(launch::async, []{ this_thread::sleep_for(chrono::seconds(1)); cout << this_thread::get_id() << " "; }); futures.push_back(move(fut)); } for (future<void>& fut : futures) { fut.wait(); } }
9.5 Mutexes
9.5.1 四种mutex
std::mutex
不循环、不支持超时std::timied_mutex
不循环、支持超时std::recursive_mutex
循环、不支持超时std::recursive_timed_mutex
循环、支持超时
mutex
既不能copy也不能move
9.5.2 mutex
的RAII类:std::lock_guard
`lock_guard`既不能copy也不能movestd::mutex m; { std::lock_guard<std::mutex> lock(m); }
9.5.3 mutex
的RAII类:std::unique_lock
`unique_lock`可以调用`lock/unlock`,可以move,支持timeout类型的mutex. 上例中试图在10ms中获取锁,如果成功或者超时则返回。如果超时,后续lock会被隐式转换为false, 如果在10ms中成功获得锁,后续lock会被隐式转换为true.`try_lock_for()`中传入的参数<=0 相当于调用 `try_lock`,能够直接返回成功还是失败。using tm_t = std::timed_mutex; rm_t m; { std::unique_lock<tm_t> lock(m, std::defer_lock); if (lock.try_lock_for(std::chrono::microseconds(10))) { /// critical section } else { /// handle timeout case } if (lock) { /// critical section } else { /// m is not locked } }
9.5.4 mutex
的RAII类:std::lock
死锁问题很容易出现在试图获取两个资源的时候。
int weight, value; std::mutex wt_mux, val_mux; { // Thread 1 std::lock_guard<std::mutex> wt_lock(wt_mux); // wt 1st std::lock_guard<std::mutex> val_lock(val_mux); // val 2nd work with weight and value // critical section } { // Thread 2 std::lock_guard<std::mutex> val_lock(val_mux); // val 1st std::lock_guard<std::mutex> wt_lock(wt_mux); // wt 2nd work with weight and value // critical section }
std::lock
能够解决这个问题:
{ // Thread 1 std::unique_lock<std::mutex> wt_lock(wt_mux, std::defer_lock); std::unique_lock<std::mutex> val_lock(val_mux, std::defer_lock); std::lock(wt_lock, val_lock); // get mutexes w/o } { // Thread 2 std::unique_lock<std::mutex> val_lock(val_mux, std::defer_lock); std::unique_lock<std::mutex> wt_lock(wt_mux, std::defer_lock); std::lock(val_lock, wt_lock); // get mutexes w/o }
如果上例中val_lock/wt_lock
已经被锁,那么会抛出异常。如果调用的参数是mutex
而且已经被锁了,那么这种行为是undefined
9.6 条件变量
std::mutex m; std::condition_variable cv; std::string data; bool ready = false; bool processed = false; void worker_thread() { // Wait until main() sends data { std::unique_lock<std::mutex> lk(m); cv.wait(lk, []{return ready;}); } std::cout << "[2] Worker thread is processing data\n"; data += " after processing"; // Send data back to main() { std::lock_guard<std::mutex> lk(m); processed = true; std::cout << "[3] Worker thread signals data processing completed\n"; } cv.notify_one(); } int main() { std::thread worker(worker_thread); data = "Example data"; // send data to the worker thread { std::lock_guard<std::mutex> lk(m); ready = true; std::cout << "[1] main() signals data ready for processing\n"; } cv.notify_one(); // wait for the worker { std::unique_lock<std::mutex> lk(m); cv.wait(lk, []{return processed;}); } std::cout << "[4] Back in main(), data = " << data << '\n'; worker.join(); }
输出如下:
[1] main() signals data ready for processing [2] Worker thread is processing data [3] Worker thread signals data processing completed [4] Back in main(), data = Example data after processing
9.7 线程局部数据
thread_local
是C++11中新引进的关键词,地位与static
, extern
相同。
thread_local unsigned int rage = 1; std::mutex cout_mutex; void increase_rage(const std::string& thread_name) { ++rage; std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "Rage counter for " << thread_name << ": " << rage << '\n'; } int main() { std::thread a(increase_rage, "a"), b(increase_rage, "b"); { std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "Rage counter for main: " << rage << '\n'; } a.join(); b.join(); return 0; }
输出如下:
Rage counter for a: 2 Rage counter for main: 1 Rage counter for b: 2
用thread_local
声明的变量在每个线程都有一份实例。
9.8 其他一些并发的支持
- 对象的线程安全初始化
std::call_once
,std::once_flag
detach
std::packaged_task
- `std::atomic
yield
,sleep
- 查询硬件支持的线程数
9.9 并发支持的总结
join/detach
async/future
- mutex与它的RAII对象
- 解决死锁问题
- 条件变量
- 线程局部数据
10. 面向所有开发者的特性总结
- 模板
>>
auto
变量- 新的for循环写法
nullptr
- unicode支持
- 统一初始化
- λ
- 模板的别名
- 并发支持