C++ 异步编程的利器std::future和std::promise

1、背景

在现代计算机系统中,许多任务可能需要花费较长时间才能完成,例如网络请求、文件读取、大规模数据计算等。如果在程序中同步地执行这些任务,会导致主线程被阻塞,整个程序在任务执行期间无法响应其他操作,用户体验极差。异步编程应运而生,它允许程序在执行耗时任务的同时,不影响其他部分的正常运行,提高了程序的效率和灵活性。这里的其他部分通常运行在不同的cpu核上,当在其它核上的任务执行完毕,通知当前线程即可,当前线程可以在这段时间片执行其它的任务。

2、std::future的基本概念与用法

std::future 可以看作是一个 "占位符" 或者 "结果容器",用于获取异步操作的结果。它提供了一种机制,使得当前线程能够在未来的某个时刻获取另一个线程中执行的异步任务的返回值。

2.1、获取异步任务结果

我们可以通过 std::async 函数来启动一个异步任务,并返回一个 std::future 对象。

cpp 复制代码
#include <iostream>
#include <future>
#include <thread>

int add(int a, int b) {
    std::this_thread::sleep_for(std::chrono::seconds(2));  // 模拟耗时操作
    return a + b;
}

int main() {
    std::future<int> result = std::async(add, 3, 5);

    // 在此处可以执行其他操作,不会被 add 函数的执行阻塞

    int sum = result.get();  // 获取异步任务的结果,此时如果任务尚未完成,会阻塞当前线程直到任务完成
    std::cout << "The sum is: " << sum << std::endl;

    return 0;
}

在上述代码中,std::async 启动了 add 函数的异步执行,并返回一个 std::future 对象 result。在 result.get() 被调用时,如果 add 函数尚未完成,主线程会被阻塞等待结果。一旦 add 函数执行完毕,result.get() 就会获取到返回值并赋值给 sum。

2.2、等待异步任务完成

除了使用 get 方法获取结果时阻塞等待,std::future 还提供了 wait 方法来单纯地等待异步任务完成,而不获取结果。

cpp 复制代码
#include <iostream>
#include <future>
#include <thread>

void print_hello() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Hello from the async task!" << std::endl;
}

int main() {
    std::future<void> result = std::async(print_hello);

    std::cout << "Before waiting..." << std::endl;
    result.wait();  // 等待异步任务完成
    std::cout << "After waiting..." << std::endl;

    return 0;
}

在这个例子中,print_hello 函数在异步线程中执行,主线程通过 result.wait() 等待其完成。在等待期间,主线程可以执行其他非依赖于 print_hello 结果的操作,但在 wait 调用处会暂停,直到异步任务结束。

2.3、查询异步任务状态

std::future 还提供了 wait_for 方法来设置一个等待时间,用于查询异步任务是否在指定时间内完成。它返回一个 std::future_status 类型的枚举值,有以下几种情况:

  • std::future_status::ready:表示异步任务已经完成,可以获取结果。
  • std::future_status::timeout:表示在等待的时间内任务未完成。
    • std::future_status::deferred:表示异步任务被延迟执行,只有在调用 get 时才会真正开始执行。
cpp 复制代码
#include <iostream>
#include <future>
#include <thread>
#include <chrono>

int multiply(int a, int b) {
    std::this_thread::sleep_for(std::chrono::seconds(3));
    return a * b;
}

int main() {
    std::future<int> result = std::async(multiply, 4, 6);

    std::cout << "Checking task status..." << std::endl;
    std::future_status status = result.wait_for(std::chrono::seconds(2));
    if (status == std::future_status::ready) {
        std::cout << "Task completed. Result: " << result.get() << std::endl;
    } else if (status == std::future_status::timeout) {
        std::cout << "Task still in progress. Waiting more..." << std::endl;
        int product = result.get();  // 再次等待并获取结果
        std::cout << "Task completed. Result: " << product << std::endl;
    } else if (status == std::future_status::deferred) {
        std::cout << "Task is deferred." << std::endl;
    }

    return 0;
}

这个例子中,设置了 2 秒的等待时间。由于 multiply 函数实际需要 3 秒才能完成,所以在 2 秒时 wait_for 返回 std::future_status::timeout,然后我们再次调用 result.get() 来获取最终结果并等待任务完成。

3、std::promise 的基本概念与用法

std::promise 则用于设置异步操作的结果或异常,它与 std::future 紧密配合,实现了异步任务结果的传递。

3.1、设置异步任务结果

我们可以在一个线程中通过 std::promise 设置结果,而在另一个线程中通过对应的 std::future 获取该结果。

cpp 复制代码
#include <iostream>
#include <future>
#include <thread>

void set_value_in_thread(std::promise<int>& prom) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    prom.set_value(42);  // 设置异步任务的结果值为 42
}

int main() {
    std::promise<int> prom;
    // 建立关联
    std::future<int> fut = prom.get_future();

    std::thread t(set_value_in_thread, std::ref(prom));

    int value = fut.get();  // 获取设置的结果值
    std::cout << "The value set in the other thread is: " << value << std::endl;

    t.join();

    return 0;
}

在上述代码中,set_value_in_thread 函数在一个新线程中运行,通过传入的 std::promise 对象 prom 设置了结果值为 42。在主线程中,通过 fut.get() 获取到了这个结果值。

3.2、设置异步任务异常

std::promise 不仅可以设置结果值,还可以设置异常。

cpp 复制代码
#include <iostream>
#include <future>
#include <thread>
#include <exception>

void set_exception_in_thread(std::promise<int>& prom) {
    try {
        throw std::runtime_error("An error occurred in the async thread.");
    } catch (...) {
        prom.set_exception(std::current_exception());  // 设置异步任务的异常
    }
}

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

    std::thread t(set_exception_in_thread, std::ref(prom));

    try {
        fut.get();  // 尝试获取结果,会抛出之前设置的异常
    } catch (const std::runtime_error& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
    }

    t.join();

    return 0;
}

在这个例子中,set_exception_in_thread 函数在异步线程中抛出了一个异常,并通过 prom.set_exception(std::current_exception()) 将异常设置到 std::promise 对象中。在主线程中,当调用 fut.get() 时,就会捕获到这个异常并进行处理。

4、std::future 和 std::promise 的关联

std::future 和 std::promise 的关联是通过 std::promise 的 get_future 方法建立的。一旦建立关联,一个线程可以通过 std::promise 设置结果或异常,而另一个线程可以通过对应的 std::future 获取结果或处理异常。这种协同工作机制使得我们可以在不同的线程之间进行高效的结果传递和错误处理,非常适合构建复杂的异步任务处理流程。例如,在一个多线程的网络服务器中,一个线程负责接收网络请求并将处理任务分配给工作线程,工作线程完成任务后可以通过 std::promise 将结果返回给主线程,主线程再通过 std::future 获取结果并响应给客户端。

5、注意事项

  • 一个 std::promise 对象只能设置一次结果或异常。一旦设置完成,再次调用 set_value 或 set_exception 将会抛出 std::future_error 异常。因此,在使用时要确保正确地设置结果或异常,避免重复设置。
  • 在使用 std::future 和 std::promise 时,要注意线程之间的同步和资源管理。例如,在获取 std::future 的结果时,如果异步任务尚未完成,当前线程会被阻塞,这可能会影响程序的性能和响应性。因此,可以合理地结合 wait_for 等方法来进行超时处理或轮询检查任务状态,避免长时间的阻塞。同时,在创建线程和使用 std::promise 等对象时,要确保正确地管理线程资源,避免出现资源泄漏等问题。例如,在使用 std::thread 时,要记得在合适的时机调用 join 或 detach 方法。
  • 在异步任务中抛出的异常,如果没有通过 std::promise 正确地设置到对应的 std::future 中,可能会导致异常被丢失或程序出现未定义行为。因此,要确保在异步任务中捕获所有可能的异常,并通过 set_exception 方法将异常传递给 std::future,以便在获取结果时能够正确地处理异常。
相关推荐
优雅的落幕7 分钟前
多线程---线程安全(synchronized)
java·开发语言·jvm
小黄编程快乐屋10 分钟前
前端小练习——大雪纷飞(JS没有上限!!!)
开发语言·前端·javascript
程序猿阿伟11 分钟前
《平衡之策:C++应对人工智能不平衡训练数据的数据增强方法》
前端·javascript·c++
爱上语文14 分钟前
请求响应:常见参数接收及封装(数组集合参数及日期参数)
java·开发语言·spring boot·后端
清风徐来辽25 分钟前
Kotlin学习:1.7.语言基础之空安全
开发语言·kotlin
CQU_JIAKE26 分钟前
926[study]Docker,DHCP
java·开发语言
程序猿进阶31 分钟前
Tomcat 都有哪些核心组件
java·开发语言·后端·面试·性能优化·tomcat·firefox
猫猫的小茶馆33 分钟前
【Linux系统】Linux内核框架(详细版本)
linux·运维·服务器·开发语言·嵌入式软件
CodeGrindstone36 分钟前
Muduo网络库剖析 --- 架构设计
网络·c++·网络协议·tcp/ip
9毫米的幻想37 分钟前
【C++】—— set 与 multiset
开发语言·c++·rpc