💻文章目录
- 📄前言
- 异步任务
- 概念
- 期待与承诺
- future
- promise
- 异常处理
- 执行异步任务
- async
- packaged_task
- 📓总结
📄前言
异步任务是多线程编程的核心,若想学习多线程设计,深入了解这些基本概念是必不可少的。如果你从未了解过这些概念,亦或者对c++异步任务的库函数有所遗忘了,不妨点进本文来学习一下。
异步任务
概念
异步任务就是在程序后台执行的任务,而异步指的是与主线程所不同步奏。用生活的例子来讲就是,我在一边看视频,一边吃饭。创建一个线程,让它执行一个函数,这就是一个简单的异步任务。你可能会觉得这也太简单了,但先不要走,其实我们要讨论的还不止如此。
C++为异步任务提供的可不止thread
,还有为了异步任务返回值而诞生的 期待(future)
与 承诺(promise)
,以及打包任务所需的 packaged_task
和 用更高级的异步任务创建工具—async
。
惯例地讨论下优缺点:
- 异步任务的优点:
- 提高程序的性能:多个线程并发运行能够显著提高程序的性能(前提是有合理的设计)。
- 提高程序响应性:一个程序可能需要等待I/O输入的同时,能够同时处理后台的各种任务(如网络数据传输)。
- 异步任务的缺点:
- 让程序调试难度提高:多线程的 bug 可能难以重现、甚至只会在特定的机器出现问题。
- 可能会导致死锁、竞态条件等问题:不合理的设计可能会导致程序的性能提高不显著,甚至导致程序无响应。
期待与承诺
如果你有使用过 thread 函数,那么你肯定会发现它是无法通过函数返回值来查看运算结果,虽然可以通过引用传参来获取返回值,但这样获取的数据在多线程情况下还得自己解决数据二义性问题,而 future 和 promise 提供了一种线程安全且方便的返回方式。
future
future 如同其名—期待,期待一个任务结果的获取,我们不需要立刻知道结果是否就绪,只需要在我们需要用到结果时才去访问。如果此时结果还未就绪,线程就会阻塞等待这个结果的获取。
用一个生活的小例子来比喻就是:我和朋友约定去公园一起玩,我在去公园的路上不知道朋友是否已经到达,只有我到了公园才知道,我当然会期待到公园的时候他就已经到达,但如果他还没到公园,我就在此地等待。相信这个例子已经足够说明期待的本质了,在C++中 future 一般与 async 、promise、package_task 等工具一起使用。
简单的函数使用演示:
#include <future>
#include <iostream>
#include <thread>
int sum(int x, int y) {
std::cout << "线程运行ing" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3)); // 模拟运行任务中。
std::cout << "线程结束" << std::endl;
return x + y;
}
int main() {
std::future<int> res = std::async(sum, 1, 9); // async的返回值是一个future。详细将在后文介绍。
std::cout << res.get() << std::endl; // res还未待续,线程阻塞等待res。
return 0;
}
promise
promise 与 future 也是一样的“人与其名”,承诺会给 future 一个结果,而这个结果可能会及时得到,也可能会非常晚才得到。promise 需要与 future 一同使用,一般通过传参使用。
函数使用:
void func(std::promise<std::string>& ip, std::promise<std::string>& msg) {
// 假设进行网络连接
std::this_thread::sleep_for(std::chrono::seconds(1));
ip.set_value("8.8.8.8"); //获取客户端ip
msg.set_value("Hello!"); // 获取客户端的信息.
}
int main() {
std::promise<std::string> get_ip, get_msg;
std::future<std::string> ip = get_ip.get_future(); // 绑定期待
std::future<std::string> msg = get_msg.get_future();
// 引用参数需要使用ref来保证其引用性质。
std::thread t1(func, std::ref(get_ip), std::ref(get_msg));
t1.detach(); //分离线程
printf("[%s] %s", ip.get().c_str(), msg.get().c_str());
return 0;
}
异常处理
future 和 promise可以用于接受异常,这也是它们的一大特点之一,如果使用 async 中发生异常,则异常会存储到它的返回值中,当调用 get() 时再次被抛出。当然使用promise也能够设置异常,然后让future 接收。
int func(int x, int y)
{
if(y == 0)
throw std::runtime_error("x / 0");
else
return x / y;
}
void errorfunc(std::promise<int>& ret)
{
try
{
int res = func(29, 0);
ret.set_value(res);
}catch(const std::runtime_error& e)
{ // promise 也能够储存异常
ret.set_exception(std::current_exception() );
}
}
int main() {
try {
std::promise<int> ret;
std::future<int> f = ret.get_future(); //绑定promise
std::thread(errorfunc, std::ref(ret)).detach();
int result = f.get(); //异常在get()函数被抛出
std::cout << result << std::endl;
}catch(const std::runtime_error& e)
{
std::cerr << "error: " << e.what() << std::endl;
}
return 0;
}
执行异步任务
async
async 是 C++ 中更智能的一种创建线程的方式,它能够自动管理线程的生命周期,并且自动控制线程的 数量(程序线程过多将不会创建),它的返回值是一个带函数返回值的 future ,可以用它来得知函数的运行结果 或 函数发生的异常。
- async 的优点:
- 自动管理线程生命周期:线程不需要自己来管理,函数将自己管理。
- 异常安全:future能接受函数中产生的异常
函数使用:
template <typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init)
{
//获取数组的长度
const unsigned long length = std::distance(first, last);
const unsigned long min_per_thread = 25;
// 分块计算数据
if(length <= 25) //递归终点
return std::accumulate(first, last, init);
else
{
Iterator mid_point = first + length / 2;
// async会自动处理线程的数量,不用担心线程无限开销。
std::future<T> first_half_result =
std::async(parallel_accumulate<Iterator, T>, mid_point, last, init); //处理后半部分
// 处理前半数据 (注意这里的init要换成T())
return first_half_result.get() + std::accumulate(first, mid_point, T());
}
}
int main() {
std::vector<int> nums(100);
// 从1开始为数组填充数据1~100
std::iota(nums.begin(), nums.end(), 1);
auto sum = parallel_accumulate(nums.begin(), nums.end(), 123);
// auto sum = std::accumulate(nums.begin(), nums.end(), 123);
return 0;
}
// 函数递归展开图
[0-100)
|
+-----------------------------------------+
| |
[0-50) [50-100)
| |
+-------------------+ +-------------------+
| | | |
[0-25) [25-50) [50-75) [75-100)
| | | |
std::accumulate std::accumulate std::accumulate std::accumulate
packaged_task
packaged_task 是用于打包异步任务的工具,它可以对普通函数、类内函数、lambda函数进行打包,然后在另一个线程中进行运行,经常用于像线程池等需要打包任务的场景。
简单的函数使用:
// 函数使用
int func(int x, int y)
{
return x + y;
}
int main() {
std::packaged_task<int(int, int)> task1(func); //包装普通函数
std::packaged_task<int()> task2([&] { return func(2, 3); }); // 包装lambda表达式
std::future<int> res1 = task1.get_future(); // packaged_task 返回值是future
std::future<int> res2 = task2.get_future();
std::thread t1(std::move(task1), 9, 9).detach;
std::thread t2(std::move(task2) ).detach;
std::cout << res1.get() << ":" << res2.get() << std::endl;
return 0;
}
📓总结
在C++中异步任务常用的工具有future、promise、async、packaged_task等,掌握它们对编写一个高效的多线程至关总要,希望本文能够对你有所帮助。
工具 | 用途 |
---|---|
std::async | 用于以简化的方式启动异步任务,自动管理线程生命周期,并自动控制线程数量,返回std::future 对象以获取任务的执行结果或异常。 |
std::future | 提供一种机制来访问异步操作的结果。当异步操作完成时,可以通过std::future 对象获取结果或捕获在异步操作中抛出的异常。 |
std::promise | 允许在某个线程中设置值或异常,这些值或异常将在未来某个时刻通过与之关联的std::future 对象被其他线程访问。 |
std::packaged_task | 封装一个可调用对象,并允许其异步执行,同时提供一个std::future 对象,以便获取该可调用对象的返回值或在执行过程中捕获的异常。 |
📜博客主页:主页
📫我的专栏:C++
📱我的github:github