C++ `std::future` 与 `std::promise`:异步任务结果的传递与获取

好,准备好进入 std::futurestd::promise 的奇妙世界了吗? 让我们开始这段异步任务之旅!

C++ std::futurestd::promise:异步任务结果的传递与获取

大家好!欢迎来到今天的“异步魔法师训练营”。今天我们要学习的是C++中实现异步编程的两个重要角色:std::futurestd::promise。 它们就像异步世界里的信使,负责传递任务的结果,让你可以在主线程中安全地接收,而不会被漫长的计算卡住。

异步编程的必要性:告别卡顿

想象一下,你正在开发一个图像处理程序。用户点击“锐化”按钮后,如果主线程直接进行复杂的图像计算,那么界面就会卡死,用户只能眼巴巴地等着,体验非常糟糕。

异步编程就是解决这个问题的利器。我们可以把耗时的图像处理任务放到另一个线程中执行,主线程继续响应用户的操作,等计算完成后再通知主线程更新图像。

std::futurestd::promise 就是实现这种机制的关键。

std::promise:承诺交付结果的生产者

std::promise,顾名思义,是一个“承诺”。 它承诺在未来的某个时刻,会提供一个特定类型的值。 我们可以把它看作是一个“结果容器”,用于存放异步任务的计算结果。

  • 创建 std::promise 对象:

    #include <iostream>
    #include <future>
    #include <thread>
    
    int main() {
        std::promise<int> myPromise; // 创建一个承诺,类型为 int
        return 0;
    }

    这行代码创建了一个 std::promise 对象,它承诺将来会提供一个 int 类型的值。

  • 获取与 std::promise 关联的 std::future

    std::future<int> myFuture = myPromise.get_future(); // 获取与 promise 关联的 future

    myPromise.get_future() 返回一个 std::future 对象,它代表了对未来结果的期望。这个 future 对象会“监听”对应的 promise 对象,一旦 promise 存储了结果,future 就可以获取到这个结果。 注意 get_future()只能调用一次。

  • 在异步任务中设置 std::promise 的值:

    std::thread t([&](std::promise<int> prom) {
        try {
            // 模拟耗时计算
            std::this_thread::sleep_for(std::chrono::seconds(2));
            int result = 42;
            prom.set_value(result); // 设置承诺的值
        } catch (...) {
            prom.set_exception(std::current_exception()); // 设置异常
        }
    }, std::move(myPromise));
    
    t.detach();

    这段代码创建了一个新的线程,在这个线程中,我们模拟了一个耗时的计算过程,并将结果 42 通过 prom.set_value(result) 设置到 promise 对象中。 注意,这里使用了std::move,因为promise只能与一个线程关联,不能被复制。
    set_exception 用于设置异常,如果异步任务执行过程中发生了异常,我们可以将异常信息设置到 promise 中,这样 future 就可以捕获到这个异常。

    set_value_at_thread_exit() 用于在线程退出时设置值。这可以确保即使线程在设置值之前退出,future 仍然可以获得结果。

std::future:等待并获取结果的接收者

std::future,顾名思义,代表了“未来”的结果。 它是一个只读的对象,用于从异步任务中获取结果。 我们可以把它看作是一个“结果接收器”。

  • 等待 std::future 的结果:

    int value = myFuture.get(); // 等待并获取 future 的结果
    std::cout << "Result: " << value << std::endl;

    myFuture.get() 会阻塞当前线程,直到 future 准备好(即对应的 promise 已经设置了值或者异常)。 一旦 future 准备好,get() 方法就会返回结果或者抛出异常。 注意get()只能调用一次。

    可以使用 wait()wait_for() 函数来等待 future 的结果,而不会阻塞。

    // 等待 future 完成,但不获取结果
    myFuture.wait();
    
    // 等待 future 完成,最多等待 1 秒
    std::future_status status = myFuture.wait_for(std::chrono::seconds(1));
    if (status == std::future_status::ready) {
        // future 已准备好
        int value = myFuture.get();
        std::cout << "Result: " << value << std::endl;
    } else if (status == std::future_status::timeout) {
        // 超时
        std::cout << "Timeout!" << std::endl;
    } else {
        // Deferred
        std::cout << "Deferred!" << std::endl;
    }

    std::future_status 枚举定义了 wait_for() 函数的返回值:

    • std::future_status::readyfuture 已准备好,可以获取结果。
    • std::future_status::timeout:等待超时。
    • std::future_status::deferredfuture 与延迟函数关联(稍后会讲到)。
  • 获取 std::future 的状态:

    if (myFuture.valid()) {
        // future 有效
        // ...
    }

    myFuture.valid() 用于检查 future 对象是否有效。 如果 future 对象是通过 promise.get_future() 创建的,或者通过 std::async 创建的,那么它是有效的。

完整示例:std::promisestd::future 的协同工作

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

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

    std::thread t([&](std::promise<int> prom) {
        try {
            std::this_thread::sleep_for(std::chrono::seconds(2));
            int result = 42;
            prom.set_value(result);
        } catch (...) {
            prom.set_exception(std::current_exception());
        }
    }, std::move(myPromise));

    t.detach();

    try {
        int value = myFuture.get();
        std::cout << "Result: " << value << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }

    return 0;
}

在这个例子中,promise 在另一个线程中设置了值,而 future 在主线程中等待并获取这个值。 这就是 std::promisestd::future 的基本工作方式。

std::async:更便捷的异步任务启动方式

std::async 是一个启动异步任务的便捷函数。 它可以自动创建 std::promisestd::future,并将任务放到另一个线程中执行(或者延迟执行,稍后会讲到)。

  • 使用 std::async 启动异步任务:

    #include <iostream>
    #include <future>
    
    int calculateSum(int a, int b) {
        std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时计算
        return a + b;
    }
    
    int main() {
        std::future<int> sumFuture = std::async(std::launch::async, calculateSum, 10, 20);
    
        std::cout << "Doing other things..." << std::endl;
    
        int sum = sumFuture.get();
        std::cout << "Sum: " << sum << std::endl;
    
        return 0;
    }

    std::async(std::launch::async, calculateSum, 10, 20) 会创建一个新的线程来执行 calculateSum(10, 20) 函数,并返回一个 std::future<int> 对象。 我们可以通过 future.get() 来获取计算结果。

  • std::launch 策略:

    std::async 函数的第一个参数是 std::launch 策略,它控制任务的执行方式:

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

    std::launch::deferred 的妙用:

    std::future<int> deferredFuture = std::async(std::launch::deferred, calculateSum, 10, 20);
    
    std::cout << "Deferred task created, but not executed yet." << std::endl;
    
    // 在这里可以做其他事情
    
    std::cout << "Now, let's get the result..." << std::endl;
    int sum = deferredFuture.get(); // 此时才会执行 calculateSum 函数
    std::cout << "Sum: " << sum << std::endl;

    使用 std::launch::deferred 策略时,calculateSum 函数不会立即执行。 只有当我们调用 deferredFuture.get() 时,它才会在当前线程中执行。 这在某些情况下可以提高性能,避免不必要的线程创建。

异常处理:异步世界里的意外情况

异步任务中也可能发生异常。 我们需要确保这些异常能够被正确地捕获和处理。

  • std::promise 中设置异常:

    std::thread t([&](std::promise<int> prom) {
        try {
            // 模拟可能抛出异常的计算
            if (rand() % 2 == 0) {
                throw std::runtime_error("Something went wrong!");
            }
            int result = 42;
            prom.set_value(result);
        } catch (...) {
            prom.set_exception(std::current_exception()); // 设置异常
        }
    }, std::move(myPromise));

    如果异步任务中发生了异常,我们可以使用 prom.set_exception(std::current_exception()) 将异常信息设置到 promise 中。 std::current_exception() 用于捕获当前线程中的异常。

  • std::future 中捕获异常:

    try {
        int value = myFuture.get();
        std::cout << "Result: " << value << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }

    future.get() 被调用时,如果 promise 中存储了异常,那么 get() 方法会抛出这个异常。 我们可以使用 try-catch 块来捕获并处理这个异常。

std::shared_future:共享的未来

有时候,我们希望多个线程共享同一个 future 对象,并获取相同的结果。 这时,可以使用 std::shared_future

  • 创建 std::shared_future

    std::promise<int> myPromise;
    std::shared_future<int> mySharedFuture = myPromise.get_future().share(); // 创建 shared_future

    myPromise.get_future().share() 会返回一个 std::shared_future 对象,它可以被多个线程共享。 注意,你需要先从 promise 获取一个普通的 future,然后再调用 share() 方法。

  • 多个线程共享 std::shared_future

    #include <iostream>
    #include <future>
    #include <thread>
    #include <vector>
    
    int main() {
        std::promise<int> myPromise;
        std::shared_future<int> mySharedFuture = myPromise.get_future().share();
    
        std::vector<std::thread> threads;
        for (int i = 0; i < 3; ++i) {
            threads.emplace_back([&](int threadId) {
                try {
                    int value = mySharedFuture.get(); // 所有线程都获取相同的结果
                    std::cout << "Thread " << threadId << " got result: " << value << std::endl;
                } catch (const std::exception& e) {
                    std::cerr << "Thread " << threadId << " Exception: " << e.what() << std::endl;
                }
            }, i);
        }
    
        std::this_thread::sleep_for(std::chrono::seconds(1)); // 确保线程启动
    
        myPromise.set_value(42); // 设置 promise 的值
    
        for (auto& t : threads) {
            t.join();
        }
    
        return 0;
    }

    在这个例子中,三个线程共享同一个 shared_future 对象,并获取相同的结果 42

总结:std::futurestd::promise 的角色

为了更好地理解 std::futurestd::promise 的角色,我们可以用一个表格来总结:

特性 std::promise std::future
角色 结果的生产者,负责设置结果或异常 结果的消费者,负责等待并获取结果或捕获异常
写权限 可写(只能设置一次值或异常) 只读
所有权 独占 独占(std::shared_future 可以共享)
关联 与一个 std::future 对象关联 与一个 std::promise 对象关联
主要方法 set_value(), set_exception(), get_future() get(), wait(), wait_for(), valid(), share()
使用场景 在异步任务中设置结果 在主线程中获取异步任务的结果

高级应用:std::packaged_task

std::packaged_task 是一个将函数或可调用对象“打包”成一个异步任务的工具。 它可以与 std::future 结合使用,方便地管理异步任务。

  • 使用 std::packaged_task

    #include <iostream>
    #include <future>
    #include <thread>
    
    int calculateSquare(int x) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        return x * x;
    }
    
    int main() {
        std::packaged_task<int(int)> task(calculateSquare); // 打包函数
        std::future<int> future = task.get_future(); // 获取 future
    
        std::thread t(std::move(task), 5); // 在线程中执行任务
    
        int square = future.get(); // 获取结果
        std::cout << "Square: " << square << std::endl;
    
        t.join();
    
        return 0;
    }

    std::packaged_task<int(int)> task(calculateSquare) 创建一个 packaged_task 对象,它将 calculateSquare 函数打包成一个异步任务。 task.get_future() 返回与该任务关联的 future 对象。 我们可以将 task 对象移动到另一个线程中执行,并通过 future.get() 获取结果。

总结

std::futurestd::promise 是 C++ 中实现异步编程的重要工具。 它们可以帮助我们避免主线程阻塞,提高程序的响应速度和用户体验。 掌握它们的使用方法,可以让你在异步编程的世界里游刃有余。 std::asyncstd::packaged_task 则提供了更便捷的异步任务启动和管理方式。 希望今天的课程能帮助你成为一名真正的“异步魔法师”!

发表回复

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