C++ `std::future`/`std::promise`的同步机制:实现异步结果的等待与获取

好的,让我们开始深入探讨 C++ std::futurestd::promise 的同步机制,以及它们如何实现异步结果的等待与获取。

讲座:C++ std::future/std::promise 同步机制详解

大家好,今天我们要深入探讨 C++ 标准库中用于异步编程的关键工具:std::futurestd::promise。理解它们的工作原理对于编写高效、并发和响应式的 C++ 应用程序至关重要。

1. 异步编程的必要性

在现代软件开发中,异步编程变得越来越重要。原因有很多:

  • 提高响应性: 避免阻塞主线程,保持用户界面的流畅和响应。
  • 并发执行: 利用多核 CPU 的优势,并行执行任务,缩短整体执行时间。
  • 资源利用率: 允许一个线程在等待 I/O 操作完成时执行其他任务,提高资源利用率。

2. std::futurestd::promise 的角色

std::futurestd::promise 是一对协同工作的类,它们充当异步操作结果的“占位符”和“生产者”。

  • std::promise 负责设置(或“承诺”)异步操作的结果。它是结果的“生产者”。
  • std::future 负责获取异步操作的结果。它是结果的“消费者”。 std::future 对象通常与一个 std::promise 对象关联,并可以等待(阻塞)直到 std::promise 设置了结果。

3. 基本用法示例

让我们通过一个简单的例子来理解它们的基本用法。

#include <iostream>
#include <future>
#include <thread>

int calculate_sum(int a, int b) {
    std::cout << "Calculating sum in a separate thread..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
    return a + b;
}

int main() {
    // 1. 创建一个 std::promise 对象
    std::promise<int> promise;

    // 2. 从 promise 中获取一个 std::future 对象
    std::future<int> future = promise.get_future();

    // 3. 创建一个线程,在该线程中执行异步操作
    std::thread worker_thread([&promise]() {
        try {
            // 执行计算,并将结果设置到 promise 中
            int result = calculate_sum(5, 3);
            promise.set_value(result);
        } catch (...) {
            // 如果发生异常,将异常设置到 promise 中
            promise.set_exception(std::current_exception());
        }
    });

    // 4. 在主线程中,等待 future 对象的结果
    std::cout << "Waiting for the result..." << std::endl;
    try {
        int sum = future.get(); // 阻塞,直到结果可用
        std::cout << "Sum: " << sum << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }

    // 5. 等待工作线程完成
    worker_thread.join();

    return 0;
}

代码解释:

  1. std::promise<int> promise;: 创建了一个 std::promise 对象,它承诺提供一个 int 类型的结果。
  2. std::future<int> future = promise.get_future();: 从 promise 对象中获取一个 std::future 对象。这个 future 对象将用于获取结果。
  3. std::thread worker_thread(...): 创建了一个新的线程,用于执行 calculate_sum 函数(模拟异步操作)。
  4. promise.set_value(result);: 在工作线程中,计算完成后,使用 promise.set_value() 将结果设置到 promise 对象中。 这会解除 future.get() 的阻塞。
  5. promise.set_exception(std::current_exception());: 如果异步操作抛出异常,使用 promise.set_exception() 将异常设置到 promise 对象中。 future.get() 会抛出相同的异常。
  6. int sum = future.get();: 在主线程中,调用 future.get() 来等待结果。 future.get() 会阻塞,直到 promise 对象设置了值或异常。
  7. worker_thread.join();: 等待工作线程完成,防止主线程提前退出。

4. 异常处理

重要的是要正确处理异步操作中可能发生的异常。如上面的代码所示,我们使用了 try...catch 块来捕获异常,并使用 promise.set_exception() 将异常传递给 future 对象。 future.get() 会重新抛出该异常,允许调用者处理它。

5. std::future 的不同获取方式

std::future 提供了几种获取结果的方式:

  • get(): 阻塞,直到结果可用,然后返回结果。 如果 promise 设置了异常,则会重新抛出该异常。 get() 只能调用一次,多次调用会导致程序崩溃(移动语义)。
  • wait(): 阻塞,直到结果可用或超时。不返回任何值。
  • wait_for(): 阻塞,直到结果可用或达到指定的超时时间。不返回任何值。 返回一个 std::future_status 枚举值,指示等待状态:
    • std::future_status::ready: 结果已准备好。
    • std::future_status::timeout: 等待超时。
    • std::future_status::deferred: future 关联的任务尚未启动(仅适用于 std::async,稍后讨论)。
  • wait_until(): 阻塞,直到结果可用或达到指定的截止时间。不返回任何值。返回一个 std::future_status 枚举值。
  • valid(): 检查 future 对象是否有效(即,是否与某个共享状态关联)。

示例:使用 wait_for() 进行超时处理

#include <iostream>
#include <future>
#include <thread>
#include <chrono>

int main() {
    std::promise<int> promise;
    std::future<int> future = promise.get_future();

    std::thread worker_thread([&promise]() {
        std::this_thread::sleep_for(std::chrono::seconds(5)); // 模拟耗时操作
        promise.set_value(42);
    });

    std::cout << "Waiting for the result with a timeout..." << std::endl;
    auto status = future.wait_for(std::chrono::seconds(3));

    if (status == std::future_status::ready) {
        std::cout << "Result is ready: " << future.get() << std::endl;
    } else if (status == std::future_status::timeout) {
        std::cout << "Timeout occurred. Result is not ready yet." << std::endl;
    } else if (status == std::future_status::deferred) {
        std::cout << "Deferred." << std::endl; // 这不会发生,因为我们手动创建了线程
    }

    worker_thread.join();

    return 0;
}

在这个例子中,wait_for() 设置了 3 秒的超时时间。如果 3 秒后结果仍然不可用,它会返回 std::future_status::timeout

6. std::shared_future

std::future 对象只能调用一次 get()。 如果需要多个线程访问相同的结果,可以使用 std::shared_futurestd::shared_future 允许多个线程安全地访问相同的结果。

#include <iostream>
#include <future>
#include <thread>
#include <vector>

int main() {
    std::promise<int> promise;
    std::future<int> future = promise.get_future();
    std::shared_future<int> shared_future = future.share(); // 将 future 转换为 shared_future

    std::vector<std::thread> threads;
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back([&shared_future, i]() {
            std::cout << "Thread " << i << " waiting for the result..." << std::endl;
            int result = shared_future.get(); // 多个线程可以同时调用 get()
            std::cout << "Thread " << i << " received result: " << result << std::endl;
        });
    }

    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟一些延迟
    promise.set_value(42);

    for (auto& thread : threads) {
        thread.join();
    }

    return 0;
}

7. std::async

std::async 是一个方便的函数,用于启动异步任务并返回一个 std::future 对象。 它简化了手动创建线程和 std::promise 的过程。

#include <iostream>
#include <future>

int calculate_product(int a, int b) {
    std::cout << "Calculating product in a separate thread..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return a * b;
}

int main() {
    // 使用 std::async 启动异步任务
    std::future<int> future = std::async(std::launch::async, calculate_product, 7, 6);

    std::cout << "Waiting for the result from std::async..." << std::endl;
    int product = future.get();
    std::cout << "Product: " << product << std::endl;

    return 0;
}

std::async 的启动策略:

  • std::launch::async: 强制在新的线程中异步执行任务。
  • std::launch::deferred: 延迟执行任务,直到调用 future.get()future.wait()。 任务将在调用 get()wait() 的线程中同步执行。
  • std::launch::any: 由系统决定是异步执行还是延迟执行。 这是默认策略。

示例:std::launch::deferred

#include <iostream>
#include <future>

int calculate_square(int x) {
    std::cout << "Calculating square in thread: " << std::this_thread::get_id() << std::endl;
    return x * x;
}

int main() {
    std::cout << "Main thread: " << std::this_thread::get_id() << std::endl;
    std::future<int> future = std::async(std::launch::deferred, calculate_square, 5);

    std::cout << "Before future.get()" << std::endl;
    int square = future.get(); // 任务将在主线程中执行
    std::cout << "Square: " << square << std::endl;
    std::cout << "After future.get()" << std::endl;

    return 0;
}

在这个例子中,由于使用了 std::launch::deferredcalculate_square 函数将在主线程中执行,而不是在单独的线程中执行。 只有在调用 future.get() 时才会执行。

8. std::packaged_task

std::packaged_task 是一种将函数或可调用对象包装成异步任务的类。 它与 std::futurestd::promise 结合使用,允许将任意函数转换为异步操作。

#include <iostream>
#include <future>
#include <thread>

int calculate_factorial(int n) {
    if (n <= 1) {
        return 1;
    }
    return n * calculate_factorial(n - 1);
}

int main() {
    // 1. 创建一个 std::packaged_task 对象
    std::packaged_task<int(int)> task(calculate_factorial);

    // 2. 从 task 中获取一个 std::future 对象
    std::future<int> future = task.get_future();

    // 3. 创建一个线程,在该线程中执行任务
    std::thread worker_thread([&task]() {
        task(5); // 调用 task,执行 calculate_factorial(5)
    });

    // 4. 在主线程中,等待 future 对象的结果
    std::cout << "Waiting for the factorial result..." << std::endl;
    int factorial = future.get();
    std::cout << "Factorial: " << factorial << std::endl;

    worker_thread.join();

    return 0;
}

9. 总结对比:std::promise, std::async, std::packaged_task

特性 std::promise std::async std::packaged_task
主要用途 手动设置异步操作的结果或异常。 启动异步任务并返回 std::future 将函数或可调用对象包装成异步任务。
线程管理 需要手动创建和管理线程。 自动管理线程(可以指定启动策略)。 需要手动创建和管理线程。
异常处理 需要手动设置异常。 自动处理异常,并将异常传递给 std::future 需要手动设置异常,或者让异常自动传播(取决于函数是否抛出异常)。
灵活性 最高的灵活性,可以完全控制异步操作的流程。 较高的灵活性,可以指定启动策略。 较高的灵活性,可以将任意函数或可调用对象转换为异步任务。
易用性 相对较低,需要手动管理线程和异常。 相对较高,简化了线程管理和异常处理。 相对较低,需要手动管理线程,但简化了将函数转换为异步任务的过程。
使用场景 需要完全控制异步操作流程,例如自定义线程池。 简单的异步任务,不需要精细的线程管理。 需要将现有函数转换为异步任务,并进行更复杂的控制。

10. 总结:掌握异步编程的关键

std::futurestd::promise 提供了一种强大的机制,用于在 C++ 中进行异步编程。 它们允许我们将耗时的操作转移到后台线程,从而保持主线程的响应性。 通过结合 std::asyncstd::packaged_task,我们可以更方便地创建和管理异步任务。 理解和掌握这些工具对于编写高效、并发的 C++ 应用程序至关重要。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注