c++11并发库之线程同步

  • 主要内容
    • 条件变量
    • future
    • async/packeged_task/promise
    • shared_future

条件变量

std::mutex _mutex;
std::condition_variable _cv;
std::deque<std::string> _data;

void thread_process_data()
{
    while(1){
        std::unique_lock<std::mutex> lk(_mutex);
        _cv.wait(lk, [](){return !_data.empty();});
        std::string str = _data.front();
        _data.pop();
        lk.unlock();
        do_something(str);
    }
}

void thread_produce_data()
{
    while(1){
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        {
            std::lock_guard<std::mutex> lk(_mutex);
            _data.push("hello cv");
        }
        _cv.notify_one();
    }  
}

等待条件的线程处理流程:

  • 获取一个std::unique_lock<std::mutex>用来保护共享变量
  • 执行wait(wait_forwait_until),这些函数的调用将以原子方式释放互斥锁并暂停线程执行
    • 在释放互斥锁之前,这里会第一次检查lambda返回值;
    • 如果条件真,则线程从wait返回并继续执行;
    • 如果条件假,则线程阻塞并同时释放互斥锁;
  • 当条件变量被通知、超时过期或出现虚假唤醒时线程被唤醒,互斥锁被原子地重新获取。然后,如果唤醒是假的,线程应该检查条件并继续等待。
    • 唤醒时重新获取互斥锁
    • 再次检查lambda返回值
    • 如果条件真,则线程继续执行;
    • 如果条件假,则线程阻塞并同时释放互斥锁;

使用std::unique_lock而不是std::lock_guard原因有二:

1、等待的线程必须在等待时解锁互斥锁,并在等待之后再次锁定它,而std::lock_guard不提供这种灵活性.

2、wait()的第一个参数要求的就是std::unique_lock的引用。

唤醒等待条件的线程:

  • 获取std::lock_gurad<std::mutex>
  • 执行修改
  • 执行notify_onenotify_all通知条件发生,等待条件的线程将被唤醒(通知不需要持有锁)

condition variable适用于某个事件会反复的发生的情景,在某些情景下线程只想等待一次事件为真,之后它将永远不会再等待这个事件发生。

future

async与future

async相比thread的不同:

  • std::thread是一个类模板,而std::async只是一个函数模板
  • std::async返回std::future对象,让异步操作创建者访问异步结果.
  • 调用std::thread总启动一个新线程,且在新线程中立即执行f
  • 调用std::async不一定会启动一个新线程,并且可以决定是否立即执行f、延迟执行、不执行

async的policy参数

  • 设置为launch::async, 将在新线程中立即执行f
  • 设置为launch::deferred,async的f将延期执行直到future对象被查询(get/wait),并且f将会在查询future对象的线程中执行,这个线程甚至不一定是调用async的线程;如果future对象从未被查询,f将永远不会执行。
  • 设置为launch::deferred |std::launch::async,与实现有关,可能立即异步执行,也可能延迟执行。
  • launch::deferred和std::launch::async都没设置,C++14中为未定义
packaged_task与future
  • 类模板std::packaged_task包装任何Callable(函数、lambda、bind表达式或其他函数对象)。
  • 与std::function不同的是,std::packaged_task提供了std::future.
  • 其威力在于只要能够访问std::future,无论std::packaged_task作为对象被传输到哪个线程中执行,都可以通过std::future获取其结果。
  • 实例化的std::packaged_task本身也是一个Callable。它可以包装在std::function对象中,也可以作为线程函数传递给std::thread,或者传递给另一个需要Callable的函数,甚至可以被直接调用。
  • std::packaged_task可以用作线程池或者任务队列的构建块,作为“消息”在线程之间传递。

包装lambda

void task_lambda()
{
    std::packaged_task<int(int,int)> task([](int a, int b) {
        return std::pow(a, b); 
    });
    std::future<int> result = task.get_future();
    //在当前线程执行std::pow(2,9)
    task(2, 9);
    std::cout << "task_lambda:\t" << result.get() << '\n';
}

包装bind

int f(int x, int y) { return std::pow(x,y); }
void task_bind()
{
    std::packaged_task<int(int,int)> task(bind(f,1,2));
    std::future<int> result = task.get_future();
    //在当前线程执行std::pow(2,9)
    task(2, 9);
    std::cout << "task_bind:\t" << result.get() << '\n';
}

作为线程函数

int f(int x, int y) { return std::pow(x,y); }
void task_thread()
{
    std::packaged_task<int(int,int)> task(f);
    std::future<int> result = task.get_future();
    //在新线程执行std::pow(2,9)
    std::thread t(std::move(task), 2, 9);
    std::cout << "task_thread:\t" << result.get() << '\n';
}

等同于:

void equal_to_task_thread()
{
    std::future<int> result = std::async(std::launch::async, f, 2, 9);
    std::cout << "equal_to_async:\t" << result.get() << '\n';
}
promise与future

std::promise<T>提供了一种设置值(类型为T)的方法,稍后可以通过关联的std::future对象读取该值。配对使用futurepromise,等待结果的线程可能会阻塞在future::wait()或者future::get(),而提供数据的线程可以使用配对的promise来设置相关的值并使future就绪。

一般的范式是:

  • 想要获取结果类型为T的线程构造std::promise< T >,并通过promise::get_future获取future对象

  • 将promise作为参数传递给新的线程函数,在新的线程函数中通过promise::set_value设置结果

  • 等待的线程通过future::wait等待结果诞生,通过future::get获取结果。

    void do_work(arg, std::promise<T> promise_arg)
    {
        do_something();
        promise_arg.set_value(T);  
    }
      
    int main()
    {
        //使用promise<T>在线程之间传递结果
        std::promise<T> one_promise;
        //构造future对象
        std::future<T> one_future = one_promise.get_future();
        //promise作为参数传递给新的线程函数
        std::thread work_thread(do_work, some_arg, std::move(one_promise));
        //等待结果
        one_future.wait();  
        //获取结果
        std::cout << "result=" << one_future.get() << '\n';
        work_thread.join();  
    }
    
  • promise不可拷贝,不能直接将one_promise作为参数传递给新的线程函数。

异常与future
  • 当使用std::async、std::packeged_task时,如果线程函数函数、被包装的函数抛出异常,与future关联的数据中将 储存异常 而非结果。

  • 对于std::promise有点不一样,如果想存储异常而非正常的结果,使用set_exception()替代set_value。

    extern std::promise<double> some_promise;
    
    try
    {
      some_promise.set_value(calculate_value());
    }
    catch(...)
    {
        //1使用current_exception提取calculate_value()中抛出的异常
        //2使用set_exception()储存异常
      some_promise.set_exception(std::current_exception());
        //或者
        //some_promise.set_exception(std::copy_exception(std::logic_error("foo ")));
    }
    
  • 当异常被储存,future将就绪,通过调用future::get()将重新抛出存储的异常。

  • 待续

std::shared_future
  • std::future处理了线程之间传输数据所需的所有同步,但是对std::future特定实例的成员函数调用不是线程安全的——本身如果从多个线程访问单个std::future对象而不进行额外的同步,那么将面临数据竞争和未定义的行为。

    std::future 模型对异步结果有唯一所有权,只有一个线程可以提取到异步结果(通过get),在第一次调用get()之后,就已经没有要提取的值了。

  • std::shared_future可以让多个线程可以等待相同的事件

    std::future是可以moveable,对异步结果的唯一所有权可以在future实例之间转移(通过移动构造语义),但是每次只有一个实例引用特定的异步结果。但是std::shared_future实例是copyable,所以可以有多个shared_future对象引用相同的关联状态.

  • 单个shared_future对象上的成员函数仍然不同步,从多个线程访问单个对象时,为了避免数据冲突,必须使用锁来保护访问

    使用它的首选方法是获取对象的副本,并让每个线程访问自己的副本。如果每个线程通过自己的std::shared_future对象访问共享异步状态,那么从多个线程访问该状态是安全的。

  • 通过future实例构造shared_future实例,必须转移所有权的方式进行

    std::promise<bool> p;
    std::future<bool> f = p.get_future();
    
    //构造shared_future方式一:
    std::shared_future<bool> sf(f);               //error
    std::shared_future<bool> sf(std::move(f));    //move触发移动构造
    //方式二:
    std::shared_future<bool> sf = f.share();
    //方式三:
    std::shared_future<bool> sf(std::move(p.get_future()));
    
    assert(!f.valid());
    assert(sf.valid()); 
    
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容