Cpp 多线程学习笔记——5. future异步编程
概述
C++通过<future>
头文件提供了一组支持异步编程的工具,使用这些工具比直接进行多线程操作更加高级、更加简便。
主要包括如下的类型:
std::future
:表示异步操作的结果,这个结果在未来可能可用,支持查询操作的状态,等待操作完成和获取结果。注意用于获取结果的get()
方法调用会阻塞当前执行流,直到结果准备就绪。std::promise
:承诺在未来提供一个可用的值,通常与std::future
配对使用,set_result()
可以设置异步操作的结果。可用get_future()
提取获得一个关联的std::future
对象。std::packaged_task
:封装一个函数或可调用对象,使其可以作为异步任务执行。可用get_future()
获得一个关联的std::future
对象。
还包括如下的函数:
std::async
:用于启动异步任务,返回一个std::future
对象代表任务的结果,注意我们必须要用变量接收这个返回值,否则当前语句会阻塞式的等待任务结束,因为只有异步任务结束才会销毁返回的临时变量!
这里
std::future
和std::promise
是成对使用的,std::packaged_task
类型和std::async
函数则是对异步编程的进一步封装和简化,可以避免显式处理std::promise
对象。
实例
我们直接用几个例子来解释异步编程的基本用法,从最简单的例子开始
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {
std::promise<double> prom;
std::future<double> fut = prom.get_future();
// 在另一个线程中设置结果
std::thread t([&prom](double x) { prom.set_value(sqrt(x)); }, 2.0);
// 等待并展示结果
std::cout << "Result: " << fut.get() << std::endl;
t.join();
return 0;
}
代码解释如下:
std::promise
代表一个承诺:- 可以以引用传递方式传递到子线程中,并通过
set_value()
方法设置值 - 可以使用
get_future()
方法获取std::future
对象
- 可以以引用传递方式传递到子线程中,并通过
std::future
代表一个异步编程的结果:可以通过get()
方法获取结果
第二个例子是std::packaged_task
的使用 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {
std::packaged_task<double(double)> task([](double x) { return sqrt(x); });
std::future<double> result = task.get_future();
// 将task移动到另一个线程中执行(加上需要传入的参数)
std::thread th(std::move(task), 2.0);
// 等待并展示结果
std::cout << "Result: " << result.get() << std::endl;
th.join();
return 0;
}
std::packaged_task
的使用比第一个例子更加简单,省略了std::promise
的定义和使用,
自动包装一个可调用对象,并把执行结果传递给std::future
对象。
第三个例子是std::async
函数的使用,仍然需要提供一个可调用对象(以及需要传入的参数)
1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
std::future<double> result =
std::async(std::launch::async, [](double x) { return sqrt(x); }, 2.0);
// 等待并展示结果
std::cout << "Result: " << result.get() << std::endl;
return 0;
}
std::async
的使用更加高级和简洁,完全不需要手动创建和管理线程,返回的std::future
对象可以获取可调用对象的结果。
std::future
std::future
对象通常不会直接创建,而是通过如下几种方式获得:
std::promise
对象的get_future()
方法std::packaged_task
对象的get_future()
方法std::async
函数的返回值
这些方式获得的对象自动与对应的异步操作相关联。
std::future
对象支持如下方法:
get()
:获取对应异步操作的结果。如果结果尚未准备好,此调用将阻塞,直到结果可用。(暂不讨论异步操作中的异常问题)wait()
:阻塞当前线程,进入无限等待状态,直到对应的异步操作完成,无返回值。wait_for
:等待指定的时间段,在这段时间内异步操作完成或超时都将结束等待。wait_until
:等待直到指定的时间点,在时间点之间异步操作完成或超时都将结束等待。valid()
:检查std::future
对象是否有效,即是否关联了一个异步操作,返回布尔值。
其中get()
和wait()
都会阻塞当前线程直到任务完成,但是wait()
可以多次调用,而get()
只允许调用一次。
可以使用wait_for(0)
实现非阻塞式的检查 1
2
3
4
5
6if(fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
// 操作已完成
}
else {
// 操作尚未完成
}
std::promise
std::promise
通常和std::future
成对出现:(使用相同的类型模板参数)
std::promise
用于在某一线程中通过set_future()
设置某个值std::future
则用于在另一线程中通过get()
获取这个值。
通常先创建std::promise
对象,然后使用get_future()
创建与之关联的std::future
对象。
由于std::promise
对象不支持,我们必须通过移动或者引用传递的方式提供给子线程。
对于更复杂的情况,则需要使用共享的std::shared_future
类型,它相比于std::future
有更弱的所有权,
允许多个线程都通过get()
获取结果。(std::future
只能调用一次get()
)
使用std::shared_future
的示例如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main() {
std::promise<int> prom;
std::thread t([&]() {
std::this_thread::sleep_for(std::chrono::seconds(2));
prom.set_value(42);
});
t.detach();
std::shared_future<int> sharedFuture = prom.get_future().share();
std::cout << "Starting tasks...\n";
// 在两个不同的任务中使用shared_future
std::thread task1([&]() { std::cout << sharedFuture.get() << std::endl; });
std::thread task2([&]() { std::cout << sharedFuture.get() << std::endl; });
task1.join();
task2.join();
std::cout << "Tasks completed.\n";
return 0;
}
虽然多个线程都可以用get()
获取结果,但是显然在结果尚未就绪时,对应的线程仍然需要陷入阻塞式的等待中。
std::packaged_task
std::packaged_task
只是对可调用对象的一次封装,省略了std::promise
的角色,
并且显然和std::promise
一样不支持拷贝,只能使用移动的方式传递给子线程,其它没什么好说的。
不同编译器对于
std::packaged_task
的实现还不一样,例如gcc允许对其进行移动,但是MSVC似乎不允许。
std::async
std::async
的调用方式有两种:
- 第一种方式需要依次传入启动策略、可调用对象、可调用对象需要的参数;
- 第一种方式只需要传入可调用对象、可调用对象需要的参数,使用默认的启动策略。
两种用法示例如下
1 | std::future<double> result1 = |
std::async
接受的启动策略通过std::launch
枚举类提供:
std::launch::async
:表示任务将立刻在另一个新线程中异步执行std::launch::deferred
:表示任务会被延迟执行,直到需要提供结果时才会在当前线程中同步执行,例如用户调用std::future::get()
或std::future::wait()
函数时。std::launch::async | std::launch::deferred
:这是上面两个策略的组合,任务既可以在一个单独的线程上异步执行,也可以选择延迟执行,取决于具体实现,不同的编译器和操作系统可能会有不同的默认行为。
在启动策略缺省时,std::async
会使用std::launch::async | std::launch::deferred
策略。
需要强调的是,我们必须使用std::future
对象来接收std::async
函数的返回值,否则产生的临时对象会直到异步操作完成才会析构,这会对主线程产生阻塞。