线程同步主要是为了解决对共享数据的竞争访问问题,所以线程同步主要是对共享数据的访问同步化(按照既定的先后次序,一个访问需要阻塞等待前一个访问完成后才能开始)。这篇文章谈到的异步编程主要是针对任务或线程的执行顺序,也即一个任务不需要阻塞等待上一个任务执行完成后再开始执行,程序的执行顺序与任务的排列顺序是不一致的。下面从任务执行顺序的角度解释下同步与异步的区别:

同步:就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
异步:调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

二、如何使用异步编程
在线程库< thread >中并没有获得线程执行结果的方法,通常情况下,线程调用者需要获得线程的执行结果或执行状态,以便后续任务的执行。那么,通过什么方式获得被调用者的执行结果或状态呢?

2.1 使用全局变量与条件变量传递结果
前面谈到的条件变量具有“通知–唤醒”功能,可以把执行结果或执行状态放入一个全局变量中,当被调用者执行完任务后,通过条件变量通知调用者结果或状态已更新,可以使用了。下面给出一个程序示例:

//future1.cpp 使用全局变量传递被调用线程返回结果,使用条件变量通知调用线程已获得结果

#include <vector>
#include <numeric>
#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
#include <condition_variable> int res = 0; //保存结果的全局变量
std::mutex mu; //互斥锁全局变量
std::condition_variable cond; //全局条件变量 void accumulate(std::vector<int>::iterator first,
std::vector<int>::iterator last)
{
int sum = std::accumulate(first, last, 0); //标准库求和函数
std::unique_lock<std::mutex> locker(mu);
res = sum;
locker.unlock();
cond.notify_one(); // 向一个等待线程发出“条件已满足”的通知
} int main()
{
std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
std::thread work_thread(accumulate, numbers.begin(), numbers.end()); std::unique_lock<std::mutex> locker(mu);
cond.wait(locker, [](){ return res;}); //如果条件变量被唤醒,检查结果是否被改变,为真则直接返回,为假则继续等待
std::cout << "result=" << res << '\n';
locker.unlock();
work_thread.join(); //阻塞等待线程执行完成 getchar();
return 0;
}

函数执行结果如下:

从上面的代码可以看出,虽然也实现了获取异步任务执行结果的功能,但需要的全局变量较多,多线程间的耦合度也较高,编写复杂程序时容易引入bug。有没有更好的方式实现异步编程呢?C++ 11新增了一个< future >库函数为异步编程提供了很大的便利。

2.2 使用promise与future传递结果
< future >头文件功能允许对特定提供者设置的值进行异步访问,可能在不同的线程中。
这些提供程序(要么是promise 对象,要么是packaged_task对象,或者是对异步的调用async)与future对象共享共享状态:提供者使共享状态就绪的点与future对象访问共享状态的点同步。< future >头文件的结构如下:

注意前面提到的共享状态,多线程间传递的返回值或抛出的异常都是在共享状态中交流的。我们知道多线程间并发访问共享数据是需要保持同步的,这里的共享状态是保证返回值或异常在线程间正确传递的关键,被调用线程可以通过改变共享状态通知调用线程返回值或异常已写入完毕,可以访问或操作了。future的状态(future_status)有以下三种:

deferred:异步操作还没开始;
ready:异步操作已经完成;
timeout:异步操作超时。
既然线程间传递返回值或异常是通过共享状态进行的,就涉及到共享状态的提供方与获取方,只有该任务或线程拥有包含共享状态的对象,其他任务或线程才能够通过共享状态的通知机制同步获取到该人物或线程的返回值或异常。我们通常使用的< thread >创建线程并不拥有共享状态,我们需要为该线程提供一个共享状态,以便后续对其返回值或异常的访问。那么,怎么为一个线程提供一个包含共享状态的对象呢?这就需要借助std::promise< T >类模板实现了,其具体用法如下:

std::promise< T >构造时,产生一个未就绪的共享状态(包含存储的T值和是否就绪的状态)。可设置T值,并让状态变为ready。也可以通过产生一个future对象获取到已就绪的共享状态中的T值。继续使用上面的程序示例,改为使用promise传递结果,修改后的代码如下:

//future2.cpp 使用promise传递被调用线程返回结果,通过共享状态变化通知调用线程已获得结果

#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono> void accumulate(std::vector<int>::iterator first,
std::vector<int>::iterator last,
std::promise<int> accumulate_promise)
{
int sum = std::accumulate(first, last, 0);
accumulate_promise.set_value(sum); // 将结果存入,并让共享状态变为就绪以提醒future
} int main()
{
// 演示用 promise<int> 在线程间传递结果。
std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
std::promise<int> accumulate_promise;
std::future<int> accumulate_future = accumulate_promise.get_future();
std::thread work_thread(accumulate, numbers.begin(), numbers.end(),
std::move(accumulate_promise));
accumulate_future.wait(); //等待结果
std::cout << "result=" << accumulate_future.get() << '\n';
work_thread.join(); //阻塞等待线程执行完成 getchar();
return 0;
}

std::promise< T >对象的成员函数get_future()产生一个std::future< T >对象,代码示例中已经展示了future对象的两个方法:wait()与get(),下面给出更多操作函数供参考:

值得注意的是,std::future< T >在多个线程等待时,只有一个线程能获取等待结果。当需要多个线程等待相同的事件的结果(即多处访问同一个共享状态),需要用std::shared_future< T >来替代std::future < T >,std::future< T >也提供了一个将future转换为shared_future的方法f.share(),但转换后原future状态失效。这有点类似于智能指针std::unique_ptr< T >与std::shared_ptr< T >的关系,使用时需要留心。

2.3使用packaged_task与future传递结果

除了为一个任务或线程提供一个包含共享状态的变量,还可以直接把共享状态包装进一个任务或线程中。这就需要借助std::packaged_task< Func >来实现了,其具体用法如下:

std::packaged_task< Func >构造时绑定一个函数对象,也产生一个未就绪的共享状态。通过thread启动或者仿函数形式启动该函数对象。但是相比promise,没有提供set_value()公用接口,而是当执行完绑定的函数对象,其执行结果返回值或所抛异常被存储于能通过 std::future 对象访问的共享状态中。继续使用上面的程序示例,改为使用packaged_task传递结果,修改后的代码如下:

//future3.cpp 使用packaged_task传递被调用线程返回结果,通过共享状态变化通知调用线程已获得结果

#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono> int accumulate(std::vector<int>::iterator first,
std::vector<int>::iterator last)
{
int sum = std::accumulate(first, last, 0);
return sum;
} int main()
{
// 演示用 packaged_task 在线程间传递结果。
std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
std::packaged_task<int(std::vector<int>::iterator,std::vector<int>::iterator)> accumulate_task(accumulate);
std::future<int> accumulate_future = accumulate_task.get_future();
std::thread work_thread(std::move(accumulate_task), numbers.begin(), numbers.end());
accumulate_future.wait(); //等待结果
std::cout << "result=" << accumulate_future.get() << '\n';
work_thread.join(); //阻塞等待线程执行完成 getchar();
return 0;
}

一般不同函数间传递数据时,主要是借助全局变量、返回值、函数参数等来实现的。上面第一种方法使用全局变量传递数据,会使得不同函数间的耦合度较高,不利于模块化编程。后面两种方法分别通过函数参数与返回值来传递数据,可以降低函数间的耦合度,使编程和维护更简单快捷。

2.4 使用async传递结果
前面介绍的std::promise< T >与std::packaged_task< Func >已经提供了较丰富的异步编程工具,但在使用时既需要创建提供共享状态的对象(promise与packaged_task),又需要创建访问共享状态的对象(future与shared_future),还是觉得使用起来不够方便。有没有更简单的异步编程工具呢?future头文件也确实封装了更高级别的函数std::async,其具体用法如下:

std::future std::async(std::launch policy, Func, Args…)
std::async是一个函数而非类模板,其函数执行完后的返回值绑定给使用std::async的std::futrue对象(std::async其实是封装了thread,packged_task的功能,使异步执行一个任务更为方便)。Func是要调用的可调用对象(function, member function, function object, lambda),Args是传递给Func的参数,std::launch policy是启动策略,它控制std::async的异步行为,我们可以用三种不同的启动策略来创建std::async:

std::launch::async参数 保证异步行为,即传递函数将在单独的线程中执行;
std::launch::deferred参数 当其他线程调用get()/wait()来访问共享状态时,将调用非异步行为;
std::launch::async | std::launch::deferred参数 是默认行为(可省略)。有了这个启动策略,它可以异步运行或不运行,这取决于系统的负载。
继续使用上面的程序示例,改为使用std::async传递结果,修改后的代码如下:

//future4.cpp 使用async传递被调用线程返回结果

#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono> int accumulate(std::vector<int>::iterator first,
std::vector<int>::iterator last)
{
int sum = std::accumulate(first, last, 0);
return sum;
} int main()
{
// 演示用 async 在线程间传递结果。
std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
auto accumulate_future = std::async(std::launch::async, accumulate, numbers.begin(), numbers.end()); //auto可以自动推断变量的类型
std::cout << "result=" << accumulate_future.get() << '\n'; getchar();
return 0;
}

从上面的代码可以看出使用std::async能在很大程度上简少编程工作量,使我们不用关注线程创建内部细节,就能方便的获取异步执行状态和结果,还可以指定线程创建策略。所以,我们可以使用std::async替代线程的创建,让它成为我们做异步操作的首选。

C++多线程并发---异步编程的更多相关文章

  1. 初步谈谈 C# 多线程、异步编程与并发服务器

    多线程与异步编程可以达到避免调用线程异步阻塞作用,但是两者还是有点不同. 多线程与异步编程的异同: 1.线程是cpu 调度资源和分配的基本单位,本质上是进程中的一段并发执行的代码. 2.线程编程的思维 ...

  2. 重新想象 Windows 8 Store Apps (44) - 多线程之异步编程: 经典和最新的异步编程模型, IAsyncInfo 与 Task 相互转换

    [源码下载] 重新想象 Windows 8 Store Apps (44) - 多线程之异步编程: 经典和最新的异步编程模型, IAsyncInfo 与 Task 相互转换 作者:webabcd 介绍 ...

  3. 重新想象 Windows 8 Store Apps (45) - 多线程之异步编程: IAsyncAction, IAsyncOperation, IAsyncActionWithProgress, IAsyncOperationWithProgress

    [源码下载] 重新想象 Windows 8 Store Apps (45) - 多线程之异步编程: IAsyncAction, IAsyncOperation, IAsyncActionWithPro ...

  4. 搞定 CompletableFuture,并发异步编程和编写串行程序还有什么区别?你们要的多图长文

    你有一个思想,我有一个思想,我们交换后,一个人就有两个思想 If you can NOT explain it simply, you do NOT understand it well enough ...

  5. C# - 多线程 之 异步编程

    异步编程 同步编程,请求响应模型,同步化.顺序化.事务化. 异步编程,事件驱动模型,以 Fire and Forget 方式实现. 异步编程模式  -§- 异步编程模型 (APM) 模式: IAsyn ...

  6. 多线程之异步编程: 经典和最新的异步编程模型,async与await

    经典的异步编程模型(IAsyncResult) 最新的异步编程模型(async 和 await) 将 IAsyncInfo 转换成 Task 将 Task 转换成 IAsyncInfo 示例1.使用经 ...

  7. 多线程之异步编程: 经典和最新的异步编程模型, IAsyncInfo 与 Task 相互转换

    经典的异步编程模型(IAsyncResult) 最新的异步编程模型(async 和 await) 将 IAsyncInfo 转换成 Task 将 Task 转换成 IAsyncInfo 示例1.使用经 ...

  8. C#多线程之异步编程

    c#中异步编程,主要有两种方法: 1.委托的异步调用: 2.Task的await,async (c# 4.5) 我们来看例子: /// <summary> /// 异步保存网页,url:网 ...

  9. c# 中的多线程和异步

    前言: 1.异步和多线程有区别吗? 答案:多线程可以说是实现异步的一种方法方法,两者的共同目的:使主线程保持对用户操作的实时响应,如点击.拖拽.输入字符等.使主程序看起来实时都保持着等待用户响应的状态 ...

随机推荐

  1. 练习1--爬取btc论坛的title和相应的url

    爬不到此论坛的html源码,应该涉及到反爬技术,以后再来解决,代码如下 import requests from lxml import etree import json class BtcSpid ...

  2. 颜色RGB值对照表

    转载自 http://www.91dota.com/?p=49# 常用颜色的RGB值及中英文名称   颜  色    RGB值 英文名 中文名   #FFB6C1 LightPink 浅粉红   #F ...

  3. Linux学习 - 输入输出重定向,管道符,通配符

    一.键盘输入读取read read [选项] [变量名] -p [显示信息] 在等待read输入时,输出提示信息 -t [秒数] 指定read输入等待时间 -n [字符数] 指定read只接收n个字符 ...

  4. Oracle中的instr函数

    最近修改某个条件,由原来输入一个数据修改为可以输入多个,如图所示: 在实现时用到了regexp_substr函数进行分割连接起来的数据,查询时还用到了instr函数进行判断,但出现了问题,当子库存输入 ...

  5. spring注解-自动装配

    Spring利用依赖注入(DI)完成对IOC容器中中各个组件的依赖关系赋值 一.@Autowired 默认优先按照类型去容器中找对应的组件(applicationContext.getBean(Boo ...

  6. tomcat之nginx调度

    # :安装nginx [root@nginx ~]# yum install nginx -y #配置 [root@nginx ~]# vim /etc/nginx/nginx.conf upstre ...

  7. Linux:expr、let、for、while、until、shift、if、case、break、continue、函数、select

    1.expr计算整数变量值 格式 :expr arg 例子:计算(2+3)×4的值 1.分步计算,即先计算2+3,再对其和乘4 s=`expr 2 + 3` expr $s \* 4 2.一步完成计算 ...

  8. 基于docker 操作mysql5.7

    1. 安装好 docker 2. 拉取 mysql5.7 镜像: docker pull mysql:5.7 其他版本 mysql:https://hub.docker.com/_/mysql?tab ...

  9. 【Linux】【Shell】【text】grep

    grep: Global search REgular expression and Print out the line. 作用:文本搜索工具,根据用户指定的"模式(过滤条件)" ...

  10. 使用matplotlib中的bar函数绘制柱状图

    使用柱状图显示三日电影的票房信息 要显示的数据为2018年12月7日-9日四场电影的票房信息 四场电影分别为:无名之辈,狗十三,毒液:知名守卫者,憨豆特工3 2018年12月7日四场电影票房分别为:[ ...