新C++标准:C++0x教程(四):面向所有开发者的特性(下)

译者: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::asyncfutures执行异步调用
  • 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内部,xValwValw的拷贝的引用,而不是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内部,wValw的拷贝的引用,而不是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内部,wValw的拷贝的引用,而不是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::bindstd::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相关的概念还有promisepackaged_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: 在getwaiting的时候执行

    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::asyncstd::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
    std::mutex m;
    {
        std::lock_guard<std::mutex> lock(m);
    }
`lock_guard`既不能copy也不能move
9.5.3 mutex的RAII类:std::unique_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
        }
    }
`unique_lock`可以调用`lock/unlock`,可以move,支持timeout类型的mutex. 上例中试图在10ms中获取锁,如果成功或者超时则返回。如果超时,后续lock会被隐式转换为false, 如果在10ms中成功获得锁,后续lock会被隐式转换为true.`try_lock_for()`中传入的参数<=0 相当于调用 `try_lock`,能够直接返回成功还是失败。
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支持
  • 统一初始化
  • λ
  • 模板的别名
  • 并发支持

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值