[Effective modern cpp] 并发API

std::async与std::thread的区别

std::thread的API并没有提供直接获取异步运行函数返回值的途径,如果异步运行的函数抛出异常,则程序就会终止。
而std::async能够通过返回std:future类型,我们能够通过返回期值的get()获得想要的信息。假设下面有返回bool类型doAsyncWork()的函数,如果想知道doAsyncWork运行返回的结果,可以通过如下方式:

bool doAsyncWork();
std::future<bool> f = std::async(doAsyncWork);

基于thread的方法要求手动管理线程耗尽,超订,负载均衡,以及新平台适配。而基于任务的方法能够通过std::async与运行时调度器避免这些问题。

std::async的启动策略

auto fut1 = std::async(f);
auto fut2 = std::async(std::launch::async,f);
auto fut3 = std::async(std::launch::async||std::async::deferred,f);

1、std::launch::async启动策略表示函数f必须以异步的方式运行,即在别的线程里运行。
2、std::launch::deferred启动策略意味着函数f只会在std::async所返回的future的get或wait得到调用才会运行,即执行会推迟至其中一个调用发生的时刻。当调用get或wait时,f会同步运行。即调用方会阻塞至f运行结束为止,如果get或wait都没有得到调用,f是不会运行的。
3、std::async的默认启动策略是上述1与2任意一种情况。即以异步或同步的方式运行皆可。

但默认启动策略有以下问题:
1、无法预知f是否会和t并发运行,因为f可能会被调度为推迟运行。
2、无法预知f是否运行在与调用fut的get或wait函数的线程不同的某线程之上。
3、连f是否会运行这件事都无法预知。

使用默认启动策略必须满足以下条件
1、任务不需要与调用get或wait的线程并发执行
2、读\写哪个线程的thread_local变量并无影响
3、或者可以给出保证在std::async返回的期值之上调用get或wait,或者可以接受任务永不执行。
4、使用wait_for或wait_unitil的代码会将任务被推迟的可能性纳入考量。

使std::thread型别对象在所有返回路径皆不可联结

可联结的线程分为:对应底层以异步方式已运行或可运行的线程。或者底层线程处于阻塞或等待调度的情况。
不可联结的线程分为:默认构造的std::thread、已移动的std::threada、已联结的std::thread、已分享的std::thread。
对可联结的线程进行析构会导致程序执行终止。因此必须保证std::thread型别对象在所有路径皆不可联结。
解决的方法可以用RAII技术。

期值析构函数行为

期值析构函数行为与期关联的共享状态决定的:
1、指涉到经由std::async启动的未推迟任务的共享状态的最后一个期值会保持阻塞,直至该任务结束。本质上,这样的一个期值的析构函数对底层线程实施了一次隐士join。特殊行为
2、其他所有期值对象的析构函数值仅仅将期值对象析构就结束了。对于底层异步运行的任务,这样做类似于对线程实施了一次隐式detach,也不会运行任何东西。对于那些被推迟任务而言,如果这一期值是最后一个,也就意味着被推迟的任务永远无法执行。
(正常行为)

特殊行为触发条件需要包含以下:
1、期值所指涉的共享状态是由于调用了std::async创建的。
2、该任务的启动策略是std::async::launch,这即可能是运行时系统的选择,也可能是调用std::async时指定的。
3、该期值是指涉到共享状态的最后一个期值。

未指涉到共享状态的期值则可以由std::packaged_task的get_future产生,此时期值的析构函数表现出正常状态

int calcValue();
std::packaged_task<int()> pt(calcValue);
auto fut = pt.get_futre();//此时的期值并没有指涉到共享状态
std::thread t(std::move(pt));//能够强制转换成std::thread

std::packaged_task是只移类型。
上述强制转换成std::thread后,t为可联结线程,在代码块结束之前如果没有进行相关join或detach操作时,则会产生可联结线程进行析构导致程序终止。

考虑针对一次性事件通信使用以void为模板型别实参的期值

条件变量配合互斥量的问题

std::condition_variable cv;
std::mutex m;
... //检测事件
cv.notify_one();//通知反应任务
std::unique_lock<std::mutex> lk(m);
cv.wait(lk);
...

以上有三个问题
1、互斥体没有很大的需求,因为检测事件与反应事件之间有可能根本不需要这种介质。例如:检测事件可能负责初始化一个全局数据结构,然后把它转交给反应事件使用。如果检测任务在初始化之后从不访问该数据结果,并且在检测任务指示已就绪之前,反应任务从不访问它,那么根据程序逻辑,这两个任务会互相阻止对方访问。
2、如果检测任务在反应任务调用wait之前就通知了条件变量,可能导致反应任务错过了通知,一直等待
3、反应任务的wait语句无法应对虚假唤醒。可以通过以下方式解决:

cv.wait(lk,[]{return 事件是否确已发生});

通过标志位的方式,需要不断地轮询,产生额外的资源消耗,轮询成本太高。

std::atomic<bool> flag(false);
...
flag = true;

...//准备反应
while(flag){//等待事件
...
}

标志位、互斥量、条件变量的方式

检测任务:

std::condition_variable cv;
std::mutex m;
bool flag(false);
...
{
std::unqiue_lock<std:mutex> g(m);
falg = true;
}
cv.notify_one();

反应任务:

{
  std::unqiue_lock<std::mutex> lk(m);
  cv.wait(lk,[]{return flag;});
}

这里互斥量Mutex是避免对标志位flag进行并发访问的情况。
这里代码看起来比较奇怪的原因在于:通知条件变量在这里的目的是告诉反应任务,它正在等待的事件可能已经发生,然而反应任务必须检查标志位才能确定。而设置标志位的目的是告诉反应任务事件确确实实已经发生了,但是检测任务仍然需要通知条件变量才能让反应任务被唤醒并去检查标志位。

使用std::promise型别对象和std::future型别对象

std::promise<void> p;

检测任务:

...
p.set_value();//通知反应任务

反应任务:

...
p.get_future().wait();//等待p对应的期值
...

std::promise型别对象和期值之间的通信通道是个一次性机制,这是它与基于条件变量和基于标志位的设计之间显著差异(可以多次通信),因此可以用来创建一个暂停状态的系统线程。

std::promise<void> p;
void react();
void detect(){
  std::thread t([]{
  				   p.get_future().wait();//暂停t直至期值被设置
  				   react();});
  ...
  p.set_value();//取消暂停t
  ...
  t.join();
}

对并发使用std::atomic,对特种内存使用volatile

一旦构造了一个std::atomic型别对象,针对它的操作就好像这些操作处于受互斥量保护的临界区域内一样,但是实际上这些操作通常会使用特殊的机器指令来实现,这些指令比使用互斥量来得更加高效。

std::atomic<int> ai(0);
ai = 10;
std::cout<<ai;//这里只保证ai的读取是原子操作
++ai;
--ai;

std::atomic对于read-modify-write操作是原子方式执行。
对于并发程序而言,std::atomic还能够对代码重新排序施加限制:在源代码中,不得将代码提前至后续会出现实体店::atomic型别变量的写入操作的位置。
常规内存的特征是:如果你向某个内存位置写入了值,该值会一直保留在那里,直到它被覆盖为止。

auto y = x;
y = x;
x = 10;
x = 20;
//编译器可以径自把这段代码试作下面这样一般
auto y = x;
x = 20;

volatile的用处就是告诉编译器,正在处理的是特种内存(特种内存最常见的是用于内存映射I、O的内存,这种内存的位置实际上是用于与外部设备如:外部传感器,显示器、打印机和网络端口等)通信,针对特种内存,编译器不要对此内存上的操作做任何优化。

volatile int x;
auto y = x;//不会被优化
y = x;
x = 10;//不会被优化
x = 20;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值