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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 【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月前
|
前端开发 JavaScript 开发者
前端开发中的异步编程:Promise 和 Async/Await 的比较与应用
在现代前端开发中,异步编程是不可或缺的技术。本文将深入探讨Promise和Async/Await这两种主流的异步编程方式,分析它们的优劣势及在实际项目中的应用场景。通过比较它们的语法、可读性和错误处理机制,帮助开发者更好地选择和理解如何在项目中高效地利用这些技术。
|
1月前
|
前端开发 JavaScript 开发者
Async 和 Await 是基于 Promise 实现
【10月更文挑战第30天】Async和Await是基于Promise实现的语法糖,它们通过简洁的语法形式,借助Promise的异步处理机制,为JavaScript开发者提供了一种更优雅、更易于理解和维护的异步编程方式。
26 1
|
26天前
|
前端开发
如何使用async/await解决Promise的缺点?
总的来说,`async/await` 是对 Promise 的一种很好的补充和扩展,它为我们提供了更高效、更易读、更易维护的异步编程方式。通过合理地运用 `async/await`,我们可以更好地解决 Promise 的一些缺点,提升异步代码的质量和开发效率。
34 5
|
26天前
|
前端开发 JavaScript
async/await和Promise在性能上有什么区别?
性能优化是一个综合性的工作,除了考虑异步模式的选择外,还需要关注代码的优化、资源的合理利用等方面。
38 4
|
6月前
|
存储 前端开发 安全
C++一分钟之-未来与承诺:std::future与std::promise
【6月更文挑战第27天】`std::future`和`std::promise`是C++异步编程的关键工具,用于处理未完成任务的结果。`future`代表异步任务的结果容器,可阻塞等待或检查结果是否就绪;`promise`用于设置`future`的值,允许多线程间通信。常见问题包括异常安全、多重获取、线程同步和未检查状态。解决办法涉及智能指针管理、明确获取时机、确保线程安全以及检查未来状态。示例展示了使用`std::async`和`future`执行异步任务并获取结果。
128 2
|
1月前
|
JSON 前端开发 JavaScript
浅谈JavaScript中的Promise、Async和Await
【10月更文挑战第30天】Promise、Async和Await是JavaScript中强大的异步编程工具,它们各自具有独特的优势和适用场景,开发者可以根据具体的项目需求和代码风格选择合适的方式来处理异步操作,从而编写出更加高效、可读和易于维护的JavaScript代码。
32 1
|
2月前
|
前端开发 JavaScript
setTimeout、Promise、Async/Await 的区别
`setTimeout` 是用于延迟执行函数的简单方法;`Promise` 表示异步操作的最终完成或失败;`Async/Await` 是基于 Promise 的语法糖,使异步代码更易读和维护。三者都用于处理异步操作,但使用场景和语法有所不同。
|
2月前
|
前端开发 JavaScript 开发者
JavaScript 中的异步编程:深入了解 Promise 和 async/await
【10月更文挑战第8天】JavaScript 中的异步编程:深入了解 Promise 和 async/await
|
2月前
|
前端开发 JavaScript UED
深入了解JavaScript异步编程:回调、Promise与async/await
【10月更文挑战第11天】深入了解JavaScript异步编程:回调、Promise与async/await
22 0
|
3月前
|
前端开发 JavaScript
解决异步问题,教你如何写出优雅的promise和async/await,告别callback回调地狱!
该文章教授了如何使用Promise和async/await来解决异步编程问题,从而避免回调地狱,使代码更加清晰和易于管理。
解决异步问题,教你如何写出优雅的promise和async/await,告别callback回调地狱!