C++同步并发操作

何时需要线程同步

  • 线程完成前,需要等待另一个线程执行
  • 线程需要等待特定事件发生
  • 线程等待某个条件变为true

线程同步的方式

  1. 持续检查共享标记
void wait_for_flag() {
    std::unique_lock lock(m);

    while (!flag) {
        lock.unlock();
        lock.lock();
    }
  
    do_something();
}
  1. 等待线程在检查间隙
void wait_for_flag() {
    std::unique_lock lock(m);

    while (!flag) {
        lock.unlock();
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); //休眠
        lock.lock();
    }
  
    //going on next
}
  1. 条件变量(condition variable)

1 条件变量使用方式

目前有以下两种方式,两者都需要与一个互斥量才能工作。

  • std::condition_variable: 仅限于和 std::mutex一起工作
  • std::condition_variable_any:可以和任何满足最低标准的互斥量一起工作,更加通用,但是体积、性能、系统资源会产生额外的开销
std::mutex lock;
std::queue<data_set> data_queue;
std::condition_variable data_cond;

void data_preparation_thread() {
    while (more_data_to_prep()) {
        data_set  const data = prep_data();
        std::lock_guard l(lock);
        data_queue.push(data);
        /**
        1. notify_one()触发一个正在执行wait()的线程,去检查条件和wait()函数的返回状态
        2. 另一种可能是,很多线程等待同一事件,对于通知他们都需要做出回应,需要使用notify_all()
        **/
        data_cond.notify_one(); 
    }
}

void data_processing_thread() {
    while (true) {
        std::unique_lock l(lock); //后续需要unlock, 因此不能用lock_guard
        /*1. 在条件满足的情况下,从wait()返回并继续持有锁。当条件不满足时,线程将对互斥量解锁,并且重新开始等待
          2. 当等待线程重新获取互斥量并检查条件时,如果它并非直接响应另一个线程的通知,这就是所谓的“伪唤醒”(spurious wakeup)。*/
        data_cond.wait(l, []{return !data_queue.empty();}); 
        /**
        另一种形式:
        if (data.queue.empty()) {
            data_cond.wait(l);
        }
        **/
        data_set data = data_queue.front();
        data_queue.pop;
        l.unlock();

        process(data);

        if (is_last_data(data)) {
            break;
        }
    }
}

2 使用期待处理一次性事件

当等待线程只等待一次,当条件为true时,它就不会再等待条件变量了,那么这种情况下使用条件变量会存在一定的浪费。

C++将这种一次性事件称为“期望”(future)。当一个线程需要等待一个特定的一次性事件时,future有以下集中应用方式:

  1. 这个线程可以周期性(较短的周期)的等待或检查,事件是否触发(检查信息板);
  2. 在检查期间也可以执行其他任务;
  3. 在等待任务期间它可以先执行另外一些任务,直到对应的任务触发,而后等待期望的状态会变为“就绪”(ready)

C++有两种期望类型: std::futurestd::shared_future
std::future的实例只能与一个指定事件相关联,而 std::shared_future的实例就能关联多个事件。后者的实现中,所有实例会在同时变为就绪状态,并且他们可以访问与事件相关的任何数据。

2.1 std::future

比如需要一个长时间的运算,但是现在并不需要关注这个值,在需要时再去获取,我们来看条件变量的方式

bool result_ok();
int get_result(); //假设result非0
int result = 0; 
std::mutex lock;

void main() {
    std::unique_lock l(lock); //1. 互斥量
    while(!result) {
        wait(l, result_ok);   //2. 条件变量, 阻塞等待
        result = get_result;   
    }
}

//3. 独立计算result的线程 
std::unique_lock l(lock);
calculate_result();
notify_all();

接下来是future的方式:

/*当任务的结果你不着急要时,你可以使用std::async启动一个异步任务。与std::thread对象等待运行方式的不同,std::async会返回一个std::future对象,这个对象持有最终计算出来的结果。当你需要这个值时,你只需要调用这个对象的get()成员函数;并且直到“期望”状态为就绪的情况下,线程才会阻塞;之后,返回计算结果*/
void main() {
    std::future<int> future_result = std::async(calculate_result);
    do_some_other() //如果暂时不需要结果,可以做些其他事情
    result = future_result.get();  //需要的时候就去获取结果,如果future未ready则阻塞
}

更多请查看std::future

async简述
函数模板 async 异步地运行函数 f (潜在地在可能是线程池一部分的分离线程中),并返回最终将保有该函数调用结果的 std::future 。

async的构造方式:

  1. 传入函数+参数: auto f2=std::async(bar,"goodbye")
  2. 传入成员函数指针+成员类的对象+成员函数参数:auto f1=std::async(&X::foo,&x,42,"hello")
  3. 传入引用:std::async(baz,std::ref(x)); // 调用baz(x)
    更多构造方式见std::async

在函数调用之前,向std::async传递一个额外参数, 参数的类型是std::launch,有以下几种取值:

  • 若设置 async 标志, 即 std::launch::async, 则 async 在新的执行线程(初始化所有线程局域对象后)执行可调用对象 f ,如同产出 std::thread(std::forward<F>(f), std::forward<Args>(args)...),除了若 f 返回值或抛出异常,则于可通过 async 返回给调用方的 std::future访问的共享状态存储结果。
  • 若设置 deferred 标志,即 std::launch::deferred, 则 async 以同 std::thread 构造函数的方式转换 f 与 args... ,但不产出新的执行线程。而是进行惰性求值:在 async 所返回的 std::future 上首次调用非定时等待函数,将导致在当前线程(不必是最初调用 std::async 的线程)中,以 args... (作为右值传递)的副本调用 f (亦作为右值)的副本。将结果或异常置于关联到该 future 的共享状态,然后才令它就绪。对同一 std::future 的所有后续访问都会立即返回结果。
  • 若 policy 中设置了 std::launch::async 和 std::launch::deferred 两个标志,则进行异步执行还是惰性求值取决于实现
auto f6=std::async(std::launch::async,Y(),1.2);  // 在新线程上执行
auto f7=std::async(std::launch::deferred,baz,std::ref(x));  // 在wait()或get()调用时执行

2.2 std::shared_future

std::future 所引用的共享状态不与另一异步返回对象共享, std::future模型独享同步结果的所有权,并且通过调用 get()函数,一次性的获取数据,这就让并发访问变的毫无意义——只有一个线程可以获取结果值,因为在第一次调用 get()后,就没有值可以再获取了,再次调用 get()会抛出异常
std::shared_future允许多个线程等候同一共享状态, 可用于同时向多个线程发信,类似 std::condition_variable::notify_all(),多个对象可以引用同一关联“期望”的结果,简而言之,std::shared_future中共享状态可以被 get()多次。
注意在每一个std::shared_future的独立对象上成员函数调用返回的结果还是不同步的,所以为了在多个线程访问一个独立对象时,避免数据竞争,必须使用锁来对访问进行保护。

int queryNumber();
void doSomething(char c, shared_future<int> f);
void check() {
    try {
        shared_future<int> f = std::async(queryNumber);

        auto f1 = std::async(std::launch::async, doSomething, '.', f);
        auto f2 = std::async(std::launch::async, doSomething, '+', f);

        f1.get();
        f2.get();
    }
    catch (const std::exception& e) {
        std::cout << "Exception: " << e.what << endl; 
    }
}

3 更多的使用 future的方式

除了前面 std::async的使用方式,还可以使用 std::package_taskstd::promisestd::package_task可以封装一个可调用对象,待后续调用,而 std::promise则可以封装一个值,待后续使用。

3.1 std::package_task

std::packaged_task<>对一个函数或可调用对象,绑定一个期望。当 std::packaged_task<> 对象被调用,它就会调用相关函数或可调用对象,将期望状态置为就绪,返回值也会被存储为相关数据。
它包装任何可调用 (Callable) 目标,包括函数、 lambda 表达式、 bind 表达式或其他函数对象,使得能异步调用它。

std::packaged_task<>的模板参数是一个函数签名,如:

int f(int x, int y) { return std::pow(x,y); }
std::packaged_task<int(int,int)> task(f)

使用std::packaged_task关联的std::future对象保存的数据类型是可调对象的返回结果类型,如示例函数的返回结果类型是int,那么声明为 std::future<int>,而不是 std::future<int(int)>

int Add(int x, int y);

void task_lambda() {
    int ret;
    std::packaged_task<int(int, int)> task([](int a, int b){return a + b;}); //使用lamba表达式包装可调用函数

    task(2, 10); //启动任务,非异步

    std::future<int> result = task.get_future();
    ret = result.get(); //获取共享状态的值

    task.reset(); //重置共享状态
    result = task.get_future();

    thread td(std::move(task), 2, 10) //异步启动
    ret = result.get();
}

3.2 std::promise

类模板 std::promise 提供存储值或异常的设施,之后通过 std::promise 对象所创建的 std::future 对象异步获得结果。注意 std::promise 只应当使用一次。

promise 是 promise-future 交流通道的“推”端:存储值于共享状态的操作同步于任何在共享状态上等待的函数(如 std::future::get )的成功返回。其他情况下对共享状态的共时访问可能冲突:例如, std::shared_future::get 的多个调用方必须全都是只读,或提供外部同步。

一对 std::promise/std::future在期望上可以阻塞等待线程,同时,提供数据的线程可以使用组合中的“承诺”来对相关值进行设置,以及将“期望”的状态置为“就绪”。

可以通过 get_future()成员函数来获取与一个给定的 std::promise相关的 std::future对象,就像是与 std::packaged_task相关。当“承诺”的值已经设置完毕(使用set_value()成员函数),对应“期望”的状态变为“就绪”,并且可用于检索已存储的值。当你在设置值之前销毁std::promise,将会存储一个异常。

在调用std::future::get()时,如果std::future对象状态不是ready,则调用的地方将一直阻塞等待。

int thread_task(std::promise<int> & pro, int i) {
    std::this_thread::sleep_for(std::chrono::miliseconds(1000));
    pro.set_value(i); //提醒 future
    return 0;
}

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

推荐阅读更多精彩内容