异步、同步、阻塞和非阻塞是常见的计算机术语。C++异步任务是为了完成什么事情,这得从这些术语入手。
一、预备知识
1.1 进程间通信
进程间的通信是通过send()
和receive()
两个基本操作完成的。
- 阻塞式发送(Blocking send)。发送方进程调用
send()
会一直阻塞至消息方进程收到信息; - 非阻塞式发送(Nonblocking send)。发送方进程调用
send()
后,就可以立即执行 - 阻塞式接收(Blocking receive) 。接收方调用
receive()
后一直阻塞,直到消息到达可用; - 非阻塞式接收(Nonblocking receieve)。 接受方调用
receive()
函数后,要么得到一个有效的结果,要么得到一个空值,即不会被阻塞;
我们可以自由组合发送和接收种类形成一个进程间通信方式。操作系统将计算机内存分为两个部分,一个是内核空间,另一个是用户空间,这是为了保护硬件设备,“危险”操作都必须经过操作系统,达到访问控制的目的。
这四种进程间的通信方式比作是不同性格的人,那么他们将是:
-
阻塞式发送(Blocking send)可以比喻为一个负责任且有些过度关心他人的人。他们在传达信息时,要确保对方完全理解并接受了这个信息,就像一个父母在孩子理解和完成他们的指示前,不会离开。
-
非阻塞式发送(Nonblocking send)可以比喻为一个忙碌且自信的人。他们传递信息后就迅速转向其他事务,相信接收者能正确理解和处理这个信息。
-
阻塞式接收(Blocking receive)可能比喻为一个专注且有耐心的人。他们会专心等待信息,直到信息到来,然后才处理其他事情,就像一个期待重要电话的人在电话到达前,都会一直等待。
-
非阻塞式接收(Nonblocking receive)可能比喻为一个灵活且善于多任务处理的人。他们不会被未到来的信息阻碍,而是会在检查信息的同时处理其他任务,就像一个忙碌的人在检查邮件的同时,也在处理其他的事务。
操作系统将计算机内存分为内核空间和用户空间的方式,可以比喻为一个组织中的分工。内核空间是像高级经理那样的角色,他们对重要的决策和资源进行管理和保护。而用户空间则是普通员工,他们执行日常任务,只能在得到权限后才能访问特定的资源或信息。
1.2 进程切换
下图演示了一个进程从线程
P
0
P_0
P0切换到
P
1
P_1
P1的切换过程:
CPU正在执行进程
P
0
P_0
P0触发中断或者发生系统调用时,CPU将会将控制权转移到操作系统内核,内核首先会将当前进程的上下文(程序计数器、寄存器等)到PCB内存块(操作系统分配给内核的内存),然后重新载入进程
P
1
P_1
P1的PCB内存块内容,至此进程
P
0
P_0
P0切换到了
P
1
P_1
P1,当再次出现中断或者系统调用时,重复以上过程。用户在进行系统调用时需要完成一系列的内存操作,这是一笔开销,对于一个UNIX 系统的现代 PC 来说, 进程切换通常至少需要花费300us 的时间。
1.3 进程状态转换
下图展示了一个进程的不同状态之间的转换关系:
- new 表示进程正在被创建
- running 进程指令正在被执行
- waiting 进程正在的等待一些事件的发生(IO完成或收到信号)
- ready 进程在等待被操作系统调度
- terminated 进程执行完毕(也可以是强行终止)
进入等待状态可以是:
- 主动调用
wait()
或者sleep()
- 系统调用
3.3 IO的非阻塞调用
我们常常提到的阻塞和非阻塞IO,其实就是在描述进程的系统调用是否会将本进程切换到“等待”状态。为什么总是和IO相连,除了主动的等待外,就只有系统调用中涉及IO操作的概念了。IO操作天然是耗时的,也就是涉及阻塞和非阻塞的。操作系统发起了一个读硬盘请求,其实是通过总线往硬盘发送了这个请求,他可以阻塞式等待IO设备返回结果,也可以非阻塞继续执行接下来的进程任务。前者将自身挂起,将CPU资源让出;后者继续执行自身任务,继续占用CPU资源。
在现在计算机中,这些物理通信操作都是异步完成的,即发出请求后,等待IO设备发出中断信号,再读取响应设备的缓冲区。大多数操作系统默认的用户级应用代码都是阻塞式的系统调用(Blocking systemcall)接口,因为这会使得应用代码编写变得容易(代码执行与编写顺序是一致的)。
当然,操作系统也会提供非阻塞IO系统调用(Nonblocking IO systemcall)。一个非阻塞系统调用不会挂起自身,而是直接返回一个值,表示有多少字节数据被成功读写。
异步IO系统调用(Asychronous system call)与非阻塞IO系统调用非常相似,也是即刻返回,但是结果会在未来的某个时间由操作系统通知调用它的线程(设置一个用户空间特殊变量值或者触发一个信号或者产生一个软中断、或者是回调函数)。
非阻塞式IO系统调用和异步IO系统调用的区别是:
- 非阻塞式IO系统调用read()结果是任何可以立即获取到的数据,这个数据可能是完整的结果,也可能是不完整的,甚至还有可能是一个空值
- 异步IO系统调用read()结果必须是完整的,但是这个完成结果的通知可以延迟到将来的某个时刻。
简单来说,非阻塞IO系统调用和异步IO都是非阻塞行为,但是返回的结果方式和内容不同而已。非阻塞系统调用(Non-blocking IO system call和asynchronous IO system call)可以用来实现线程级别的IO并发,与多进程实现的IO并发具有更低的内存消耗和进程切换开销。
标准库提供了异步任务返回值和异常的便利接口。这些值的属性是异步任务共享的,拥有std::future
和std::shared_future
的实例线程对共享变量进行查看、等待的等其他操作。
二、标准库Future库
关于标准库Future相关库如下:
2.1 std::promise
头文件在std::future
的std::promise
是一个提供存储值或者异常的场所,结合对应点的std::future
可以异步获取这些值或者异常。下面是promise
模板的定义:
template< class R >
class promise;
template< class R >
class promise<R&>;
template<>
class promise<void>;
- 模板基类
- 非空特化,两个线程之间信息交换(通过R实例)
- 空特化,两个线程之间信息交换(无状态事件)
下面模板实例化之后类的构造函数:
promise();(1) //(since C++11)
template< class Alloc >
promise( std::allocator_arg_t, const Alloc& alloc );//(2) (since C++11)
promise( promise&& other ) noexcept;//(3) (since C++11)
promise( const promise& other ) = delete;//(4) (since C++11)
可以看到,std::promise
对象是禁止拷贝构造,但是允许了移动构造。
每一个promise
对象都与一个共享状态关联(包括状态信息、结果),一个promise
可能对共享状态做以下三件事情:
make ready
:存储值或异常,将状态标记为就绪并解除所有正在等待这个共享状态的future
的所有线程;release
: 放弃对共享状态的引用,如果是最后一个引用,共享状态会销毁。除非这是一个std::async
创建的共享状态而且没有准备好,否则操作不会阻塞;abandon
:promise
存储了异常并将状态标记为就绪,然后release
这个变量。
那么它有什么方法呢?
-
获取结果
get_future
返回一个与promise
具有相同共享状态的future
对象(像是绑定) -
设置结果
方法 | 含义 |
---|---|
set_value | sets the result to specific value |
set_value_at_thread_exit | sets the result to specific value while delivering the notification only at thread exit |
set_exception | sets the result to indicate an exception |
set_exception_at_thread_exit | sets the result to indicate an exception while delivering the notification only at thread exit |
2.2 std::future
template< class T > class future;
template< class T > class future<T&>;
template<> class future<void>;
std::future提供了一个访问异步结果的机制:
- 异步操作(
std::async std::packaged_task std::promise
)可以为创建者提供一个std::future
对象; - 异步操作的创建者可以用各种方法查询、等待或者从
std::future
提取值 - 当异步操作已经准备好发送数据给创建者时,他还能够改变创建者相关的
std::promise
共享状态(std::promise::set_value
)
看几个重要的方法:
-
获取结果
get返回异步结果 -
状态
操作 | 含义 |
---|---|
valid | checks if the future has a shared state |
wait | waits for the result to become available |
wait_for | waits for the result, returns if it is not available for the specified timeout duration |
wait_until | waits for the result, returns if it is not available until specified time point has been reached |
2.3 std::packaged_task
template< class > class packaged_task; //not defined
template< class R, class ...ArgTypes >
class packaged_task<R(ArgTypes...)>;
std::packaged
封装了可调用对象,使得他们能够异步调用。他将会返回std::future
对象。
方法 | 含义 |
---|---|
get_future | 返回promise对应的future结果 |
operator() | 执行函数 |
make_ready_at_thread_exit | 使得结果在退出时才准备好 |
reset | 重置状态并放弃存储上次执行的结果 |
2.4 std:::async
template< class Function, class... Args>
std::future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>
async( Function&& f, Args&&... args );//(since C++11)(until C++17)
template< class Function, class... Args>
std::future<std::invoke_result_t<std::decay_t<Function>,
std::decay_t<Args>...>>
async( Function&& f, Args&&... args );//(since C++17)(until C++20)
template< class Function, class... Args>
[[nodiscard]]
std::future<std::invoke_result_t<std::decay_t<Function>,
std::decay_t<Args>...>>
async( Function&& f, Args&&... args );//(since C++20)
template< class Function, class... Args >
std::future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>
async( std::launch policy, Function&& f, Args&&... args );//(since C++11)(until C++17)
template< class Function, class... Args >
std::future<std::invoke_result_t<std::decay_t<Function>,
std::decay_t<Args>...>>
async( std::launch policy, Function&& f, Args&&... args );//(since C++17)(until C++20)
template< class Function, class... Args >
[[nodiscard]]
std::future<std::invoke_result_t<std::decay_t<Function>,
std::decay_t<Args>...>>
async( std::launch policy, Function&& f, Args&&... args );//(since C++17)
总结起来就三个参数:
f
可调用对象args...
传递给f的参数policy
掩码值,std::launch::async
异步求值std::launch::deferred
惰性求值
返回值都是一个std::future
对象。
三、实例
3.1 线程之间传递结果
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); // Notify future
}
int main()
{
// Demonstrate using promise<int> to transmit a result between threads.
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));
// future::get() will wait until the future has a valid result and retrieves it.
// Calling wait() before get() is not needed
//accumulate_future.wait(); // wait for result
std::cout << "result=" << accumulate_future.get() << '\n';
work_thread.join(); // wait for thread completion
}
accumulate
接受一个std::vector
范围,并传入一个std::promise
,目的是计算数组和。注意,promise
这里是两个线程沟通的媒介且使用了移动构造函数。观察std::promise<int>
的时候定义,可以看到本次通信交换的共享数据类型是一个int
。std::promise
和std::future
的模板参数一致,使用auto
会比较简单。
3.2 线程间传递信号
void do_work(std::promise<void> barrier)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
barrier.set_value();
}
int main()
{
// Demonstrate using promise<void> to signal state between threads.
std::promise<void> barrier;
std::future<void> barrier_future = barrier.get_future();
std::thread new_work_thread(do_work, std::move(barrier));
barrier_future.wait();
new_work_thread.join();
}
如果你只是希望传递一个信号,那么模板参数可以是空。
[1] https://maples7.com/2016/08/24/understand-sync-async-and-blocking-non-blocking/
[2] https://www.zhihu.com/question/19732473