【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索(二)

简介: 【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索

【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索(一)https://developer.aliyun.com/article/1464316


3.3 std::async在高级应用中的应用 (Applications of std::async in Advanced Use Cases)

std::async不仅仅能用于简单的异步任务,还可以在一些高级的应用场景中发挥作用。这些应用通常涉及到大量的计算或者需要并行处理的场景。

3.3.1 并行算法

在需要处理大量数据的情况下,我们可以使用std::async来并行化算法。例如,假设我们需要对一个数组进行排序,我们可以将数组分成两半,然后在两个异步任务中分别排序这两半,最后再合并结果。

这是一个并行排序的示例:

#include <algorithm>
#include <future>
#include <vector>
template <typename T>
void parallel_sort(std::vector<T>& v) {
    if (v.size() <= 10000) {  // 对于小数组,直接排序
        std::sort(v.begin(), v.end());
    } else {  // 对于大数组,分成两半并行排序
        std::vector<T> v1(v.begin(), v.begin() + v.size() / 2);
        std::vector<T> v2(v.begin() + v.size() / 2, v.end());
        std::future<void> fut = std::async([&v1] { parallel_sort(v1); });
        parallel_sort(v2);
        fut.get();
        std::merge(v1.begin(), v1.end(), v2.begin(), v2.end(), v.begin());
    }
}

在这个示例中,我们定义了一个并行排序函数parallel_sort。如果数组的大小小于10000,我们直接对数组进行排序;如果数组的大小大于10000,我们将数组分成两半,然后在一个异步任务中排序第一半,在主线程中排序第二半,最后合并结果。

3.3.2 后台任务

在一些情况下,我们可能需要在后台执行一些任务,这些任务可能需要很长时间才能完成。例如,我们可能需要在后台下载一个文件,或者执行一些复杂的计算。std::async提供了一种简单的方式来处理这种情况。

下面是一个在后台下载文件的示例:

#include <future>
#include <iostream>
#include <string>
std::string download_file(const std::string& url) {
    // 用你的下载库下载文件...
    return "file content";
}
int main() {
    std::future<std::string> fut = std::async(download_file, "http://example.com/file");
    // 在此处执行其他任务...
    std::string file_content = fut.get();
    std::cout << "Downloaded file content: " << file_content << "\n";
    return 0;
}

在这个示例中,我们在一个异步任务中下载一个文件,然后继续执行其他任务。当我们需要文件的内容时,我们调用fut.get()获取结果。

3.3.3 异步日志系统

在许多系统中,日志系统是一个关键的组件,用于记录程序的运行情况。然而,写入日志可能是一个时间开销很大的操作,特别是当我们需要写入大量日志时。使用std::async,我们可以将日志写入操作放在一个单独的线程中,从而避免阻塞主线程。

下面是一个异步日志系统的简单示例:

#include <future>
#include <iostream>
#include <string>
#include <vector>
void write_log(const std::string& log) {
    // 在这里写入日志,例如:
    std::cout << "Writing log: " << log << std::endl;
}
std::future<void> log_async(const std::string& log) {
    return std::async(std::launch::async, write_log, log);
}
int main() {
    std::vector<std::future<void>> futures;
    futures.push_back(log_async("Start program"));
    // 执行其他任务...
    futures.push_back(log_async("End program"));
    // 等待所有异步日志任务完成
    for (auto& future : futures) {
        future.get();
    }
    return 0;
}

在这个示例中,我们在一个异步任务中写入日志,然后立即返回,不等待日志写入完成。这样,我们就可以在不阻塞主线程的情况下写入日志。

3.3.4 实时计算系统

在一些实时计算系统中,我们可能需要在一定的时间内完成一些任务,否则就需要中止这些任务。std::asyncstd::future提供了一种简单的方式来实现这种需求。

下面是一个实时计算系统的示例:

#include <future>
#include <iostream>
#include <chrono>
int calculate() {
    // 在这里执行一些复杂的计算...
    return 42;
}
int main() {
    std::future<int> fut = std::async(std::launch::async, calculate);
    std::chrono::milliseconds span(100);  // 最多等待100毫秒
    if (fut.wait_for(span) == std::future_status::ready) {
        int result = fut.get();
        std::cout << "Result is " << result << "\n";
    } else {
        std::cout << "Calculation did not finish in time\n";
    }
    return 0;
}

在这个示例中,我们在一个异步任务中执行计算,然后等待最多100毫秒。如果计算在这个时间内完成,我们就获取结果;否则,我们就打印一条消息,表示计算没有在时间内完成。

四、std::packaged_task:封装可调用目标的功能

4.1 std::packaged_task的基本原理和结构

std::packaged_task是C++11引入的一种工具,它的主要作用是封装可调用的对象,如函数、lambda表达式、函数指针或函数对象,这使得我们可以在不同的上下文或线程中执行这些任务。std::packaged_task对异步操作进行抽象,可以将其视为一个“包裹”,其中包含了异步操作的所有必要信息。

基本原理

std::packaged_task的内部,其将所封装的可调用对象和一个std::future对象关联在一起。当我们调用std::packaged_task对象时,它会执行所封装的任务,然后将结果存储在std::future中。这样,我们就可以通过这个std::future来获取任务的结果,无论任务是在哪个线程中完成的。

// 创建一个 packaged_task,它将 std::plus<int>() 封装起来
std::packaged_task<int(int, int)> task(std::plus<int>());
// 获取与 task 关联的 future
std::future<int> result_future = task.get_future();
// 在另一个线程中执行 task
std::thread(std::move(task), 5, 10).detach();
// 在原线程中,我们可以从 future 中获取结果
int result = result_future.get();  // result == 15
结构

std::packaged_task是一个模板类,其模板参数是可调用对象的类型。例如,如果我们有一个返回void并接受一个int参数的函数,那么我们可以创建一个std::packaged_task的对象。

std::packaged_task主要包含以下几个公有成员函数:

  • 构造函数:用于构造std::packaged_task对象,并将可调用对象封装在内部。
  • operator(): 用于调用封装的任务。
  • valid(): 用于检查std::packaged_task是否含有一个封装的任务。
  • get_future(): 用于获取与std::packaged_task关联的std::future对象。
  • swap(): 用于交换两个std::packaged_task对象的内容。

通过合理地使用std::packaged_task,我们可以更好地管理异步任务,并从任何地方获取任务的结果。在C++并发编程中,这是一种非常有用的工具。

4.2 std::packaged_task的使用场景和示例代码

std::packaged_task在多线程编程中有广泛的应用,主要适用于那些需要异步执行任务并获取结果的场景。下面是几个使用std::packaged_task的典型场景:

  1. 异步任务执行:当你需要在另一个线程中执行任务,并且希望在当前线程中获取结果时,你可以使用std::packaged_task
  2. 任务队列:你可以创建一个std::packaged_task的队列,将任务放入队列中,并由一个或多个工作线程来执行这些任务。
  3. Future/Promise模型:你可以使用std::packaged_task实现Future/Promise模型,其中std::future用于获取结果,std::packaged_task用于执行任务并存储结果。

以下是一个std::packaged_task的使用示例:

#include <iostream>
#include <future>
#include <thread>
// 一个要在子线程中执行的函数
int calculate(int x, int y) {
    return x + y;
}
int main() {
    // 创建一个packaged_task,将calculate函数封装起来
    std::packaged_task<int(int, int)> task(calculate);
    // 获取与task关联的future
    std::future<int> result = task.get_future();
    // 创建一个新线程并执行task
    std::thread task_thread(std::move(task), 5, 10);
    
    // 在主线程中,我们可以从future中获取结果
    int result_value = result.get();
    std::cout << "Result: " << result_value << std::endl;  // 输出: Result: 15
    task_thread.join();
    return 0;
}

在这个例子中,我们创建了一个std::packaged_task对象task,它将calculate函数封装起来。然后我们在一个新的线程中执行task,并在主线程中通过std::future获取结果。这样我们就能够异步地执行任务,并在需要的时候获取结果。

4.3 std::packaged_task在高级应用中的应用

std::packaged_task在复杂的多线程环境中有很多高级应用,比如任务队列、线程池和异步任务链等。以下将简要介绍几个应用案例。

任务队列

任务队列是一种常见的多线程设计模式,允许多个生产者线程提交任务,然后由一个或多个消费者线程执行这些任务。std::packaged_task非常适合用来实现任务队列,因为它可以将任意的可调用对象封装成一个统一的接口。

#include <queue>
#include <future>
#include <mutex>
// 任务队列
std::queue<std::packaged_task<int()>> tasks;
std::mutex tasks_mutex;
// 生产者线程
void producer() {
    // 创建一个packaged_task
    std::packaged_task<int()> task([]() { return 7 * 7; });
    // 将task添加到任务队列中
    std::lock_guard<std::mutex> lock(tasks_mutex);
    tasks.push(std::move(task));
}
// 消费者线程
void consumer() {
    // 从任务队列中取出一个task并执行
    std::lock_guard<std::mutex> lock(tasks_mutex);
    if (!tasks.empty()) {
        std::packaged_task<int()> task = std::move(tasks.front());
        tasks.pop();
        task();
    }
}
线程池

线程池是一种常见的多线程设计模式,它创建一定数量的线程,并复用这些线程来执行任务。std::packaged_task可以用来实现线程池中的任务,因为它可以在一个线程中执行任务,并在另一个线程中获取结果。

异步任务链

异步任务链是一种设计模式,其中一个任务的结果被用作下一个任务的输入。std::packaged_task可以用来实现异步任务链,因为它可以在任务完成时将结果存储在std::future中,然后这个std::future可以被下一个任务用来获取结果。

// 第一个任务
std::packaged_task<int()> task1([]() { return 7 * 7; });
std::future<int> future1 = task1.get_future();
// 第二个任务,它的输入是第一个任务的结果
std::packaged_task<int(int)> task2([](int x) { return x + 1; });
std::future<int> future2 = task2.get_future();
// 在一个线程中执行第一个任务
std::thread(std::move(task1)).detach();
// 在另一个线程中执行第二个任务
std::thread([&]() {
    task2(future1.get());
}).detach();
// 获取第二个任务的结果
int result = future2.get();

在这个例子中,我们创建了一个异步任务链,其中第一个任务计算7 * 7,然后第二个任务将结果加一。我们在两个不同的线程中执行这两个任务,然

后在主线程中获取最终的结果。这展示了std::packaged_task在高级并发编程中的强大能力。

特性\模型 任务队列 线程池 异步任务链
适用场景 需要在多个线程中分发和执行任务,适合生产者-消费者模型 需要优化任务执行性能,避免频繁创建和销毁线程,适合并发高、任务量大的场景 任务之间存在依赖关系,下游任务需要使用上游任务的结果,适合数据处理和计算密集型任务
资源使用 可根据任务队列的长度动态调整线程数量,资源使用灵活 固定数量的线程,提前创建并复用,资源使用稳定 每个任务可能在不同的线程中执行,资源使用灵活,但可能需要更多的线程间同步
任务管理 任务通过队列进行管理,可以按照先进先出或优先级等策略调度任务 任务通常由线程池内部的任务队列进行管理,线程池负责任务的调度和执行 任务的管理需要根据任务之间的依赖关系进行,通常需要更复杂的逻辑
结果获取 通过std::future获取结果,可以异步获取,或阻塞等待结果 通过std::future获取结果,可以异步获取,或阻塞等待结果 通过std::future获取结果,可以异步获取,或阻塞等待结果,下游任务可以直接使用上游任务的结果
错误处理 错误通常需要在执行任务的线程中捕获,并通过std::future传递给获取结果的线程 错误通常需要在执行任务的线程中捕获,并通过std::future传递给获取结果的线程 错误通常需要在执行任务的线程中捕获,并通过std::future传递给获取结果的线程,错误可能会导致整个任务链的中断

五、std::promise:异步操作的结果承诺 (std::promise: Promise of Asynchronous Operation Results)

5.1 std::promise的基本原理和结构 (Basic Principles and Structure of std::promise)

std::promise 是一个在 C++11 及其后续版本中被引入的并发编程工具,它允许我们在一个线程中设置一个值或异常,然后在另一个线程中获取这个值或异常。这样的特性使得 std::promise 成为了一种强大的线程间通信手段。

设想一下,你正在组织一场盛大的晚会,而你需要为你的客人承诺他们会得到美味的食物。在这种情况下,你可能会雇佣一位厨师来准备食物。你向客人承诺(promise)将会有美食,而厨师则在背后工作,尽力满足你的承诺。在 C++ 中,这个过程就像一个线程(厨师)在工作,而另一个线程(你)在等待结果。

现在,让我们深入 std::promise 的底层原理和结构。

基本原理

std::promise 的基本原理很简单。当你创建一个 std::promise 对象时,你可以给它一个值或者一个异常。这个值或者异常可以被一个与该 promise 关联的 std::future 对象获取。这就是一个标准的“生产者-消费者”模型,在这个模型中,promise 是生产者,而 future 是消费者。

结构

std::promise 是一个模板类,它有一个模板参数 T,表示承诺的值的类型。一个 std::promise 对象可以通过它的成员函数 set_value 来设置一个值,或者通过成员函数 set_exception 来设置一个异常。这些值或异常可以通过与之关联的 std::future 对象来获取。

下面是一个 std::promise 的简单使用示例:

#include <iostream>
#include <future>
#include <thread>
void my_promise(std::promise<int>& p) {
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些工作
    p.set_value(42); // 设置值
}
int main() {
    std::promise<int> p;
    std::future<int> f = p.get_future(); // 获取 future
    std::thread t(my_promise, std::ref(p)); // 在新线程中运行函数
    std::cout << "Waiting for the answer...\n";
    std::cout << "The answer is " << f.get() << '\n'; // 获取值
    t.join();
    return 0;
}

这个示例中,my_promise 函数在一个新线程中运行,并设置 promise 的值为 42。主线程等待future 的值,然后打印出来。注意到,主线程会阻塞在 f.get() 处,直到 promise 的值被设置。

在下一节中,我们将详细介绍 std::promise 的使用场景和示例代码。

5.2 std::promise的使用场景和示例代码 (Use Cases and Example Code for std::promise)

使用场景

std::promise 最常见的使用场景是在多线程环境中进行线程间通信,尤其是当你需要在一个线程中设置一个值(或者一个异常),并在另一个线程中获取这个值(或异常)时。

此外,std::promise 可以用于以下场景:

  1. 异步任务:当你需要运行一个可能会花费很长时间的任务,并且你不想等待这个任务完成,你可以使用 std::promise 在一个新线程中运行这个任务,并在主线程中获取结果。
  2. 数据流管道:你可以使用一系列的 std::promise 和 std::future 对象来创建一个数据流管道,其中每个线程都是管道的一部分,并且每个线程都通过 std::promise 对象提供数据,然后通过 std::future 对象获取数据。
示例代码

让我们通过一个例子来展示如何使用 std::promise。在这个例子中,我们将使用一个 promise 来传递一个从新线程中计算出来的结果。

#include <iostream>
#include <future>
#include <thread>
// 这个函数将会在一个新线程中被运行
void compute(std::promise<int>& p) {
    int result = 0;
    // 做一些计算...
    for (int i = 0; i < 1000000; ++i) {
        result += i;
    }
    // 计算完成,设置 promise 的值
    p.set_value(result);
}
int main() {
    // 创建一个 promise 对象
    std::promise<int> p;
    // 获取与 promise 关联的 future 对象
    std::future<int> f = p.get_future();
    // 在新线程中运行 compute 函数
    std::thread t(compute, std::ref(p));
    // 在主线程中获取结果
    std::cout << "The result is " << f.get() << std::endl;
    // 等待新线程完成
    t.join();
    return 0;
}

在这个例子中,我们在新线程中运行了一个可能会花费很长时间的计算任务,并使用了一个 promise 来传递计算结果。在主线程中,我们通过 future 对象获取了这个结果。当我们调用 f.get() 时,主线程会阻塞,直到新线程完成计算并设置 promise 的值。

5.3 std::promise在高级应用中的应用 (Applications of std::promise in Advanced Use Cases)

std::promise 不仅仅可以在基础的多线程编程中使用,它也有一些高级应用场景,比如与其它并发工具结合使用以提高程序的性能和效率。以下是两个使用 std::promise 的高级应用场景:

高级应用一:链式异步任务

在某些情况下,你可能需要执行一系列的异步任务,其中每个任务的输入都依赖于前一个任务的输出。这种情况下,你可以创建一个 promise 和 future 的链,每个任务都有一个输入 future 和一个输出 promise,这样就可以确保任务的执行顺序,并且可以方便地获取每个任务的结果。

例如,下面的代码展示了如何使用 promise 和 future 的链来执行一系列的异步任务:

#include <iostream>
#include <future>
#include <thread>
void chain_task(std::future<int>& f, std::promise<int>& p) {
    int input = f.get(); // 获取输入
    int output = input * 2; // 执行一些计算
    p.set_value(output); // 设置输出
}
int main() {
    // 创建 promise 和 future 的链
    std::promise<int> p1;
    std::future<int> f1 = p1.get_future();
    std::promise<int> p2;
    std::future<int> f2 = p2.get_future();
    // 在新线程中运行异步任务
    std::thread t1(chain_task, std::ref(f1), std::ref(p2));
    std::thread t2(chain_task, std::ref(f2), std::ref(p1));
    // 设置初始输入
    p1.set_value(42);
    // 获取最终结果
    std::cout << "The final result is " << f1.get() << std::endl;
    // 等待新线程完成
    t1.join();
    t2.join();
    return 0;
}

高级应用二:与其它并发工具结合使用

std::promise 可以与 C++ 标准库中的其它并发工具结合使用,比如 std::async、std::packaged_task、std::thread 等,来创建更复杂的并发模式。

例如,你可以使用 std::async 来启动一个异步任务,并使用 std::promise 来传递任务的结果。你也可以使用 std::packaged_task 来封装一个可以在新线程中运行的任务,并使用 std::promise 来设置任务的结果。在这种情况下,std::promise 可以提供更高级的线程间通信机制,使你可以在不同的线程中共享数据和状态。

以上就是 std::promise 在高级应用中的一些使用场景,希望可以帮助你更好地理解并使用这个工具。在下一章中,我们将介绍 std::future、std::async、std::packaged_task 和 std::promise 的比较和选择。


【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索(三)https://developer.aliyun.com/article/1464318

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
6天前
|
C++
C++11 std::lock_guard 互斥锁
C++11 std::lock_guard 互斥锁
12 0
|
6天前
|
前端开发 Go
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
【5月更文挑战第3天】Go语言通过goroutines和channels实现异步编程,虽无内置Future/Promise,但可借助其特性模拟。本文探讨了如何使用channel实现Future模式,提供了异步获取URL内容长度的示例,并警示了Channel泄漏、错误处理和并发控制等常见问题。为避免这些问题,建议显式关闭channel、使用context.Context、并发控制机制及有效传播错误。理解并应用这些技巧能提升Go语言异步编程的效率和健壮性。
30 5
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
|
6天前
|
C++
【C++】std::string 转换成非const类型 char* 的三种方法记录
【C++】std::string 转换成非const类型 char* 的三种方法记录
9 0
|
6天前
|
编译器 C语言 C++
【C++的奇迹之旅(二)】C++关键字&&命名空间使用的三种方式&&C++输入&输出&&命名空间std的使用惯例
【C++的奇迹之旅(二)】C++关键字&&命名空间使用的三种方式&&C++输入&输出&&命名空间std的使用惯例
|
6天前
|
安全 程序员 C++
【C++ 基本知识】现代C++内存管理:探究std::make_系列函数的力量
【C++ 基本知识】现代C++内存管理:探究std::make_系列函数的力量
109 0
|
6天前
|
设计模式 安全 算法
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
19 0
|
4天前
|
测试技术 C++
C++|运算符重载(3)|日期类的计算
C++|运算符重载(3)|日期类的计算
|
5天前
|
C语言 C++ 容器
C++ string类
C++ string类
9 0
|
6天前
|
C++ Linux