C++ `task` 类型:异步操作的统一封装与组合

C++ task 类型:异步操作的统一封装与组合 (讲座模式)

各位靓仔靓女,欢迎来到今天的C++异步编程小课堂!今天我们不聊虚的,直接上干货,主题就是C++11引入的task类型。这玩意儿就像异步世界里的瑞士军刀,能把各种乱七八糟的异步操作收拾得服服帖帖,还能像搭积木一样组合起来,简直不要太方便!

为什么要用task

在没有task之前,C++的异步编程简直就是一场噩梦。各种线程、锁、条件变量满天飞,代码写得比意大利面条还乱。而且,异常处理也是个大问题,一不小心就崩溃给你看。

task的出现就是为了解决这些痛点。它可以:

  • 统一封装异步操作: 把一个异步操作包装成一个task对象,就像把一团乱麻整理成一个整齐的线团。
  • 简化异常处理: task会默默地把异步操作中的异常存起来,等你用get()取结果的时候再抛出来,避免程序直接崩溃。
  • 方便组合异步操作: 可以像搭积木一样,把多个task组合起来,形成更复杂的异步流程。

简单来说,task就是让异步编程变得更简单、更安全、更可控。

task的基本用法:把函数变成异步任务

最简单的用法就是用std::async把一个函数变成一个taskstd::async会自动把函数放到一个独立的线程里去执行,然后返回一个task对象。

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

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

int main() {
    std::future<int> future_result = std::async(std::launch::async, calculate_sum, 10, 20);

    std::cout << "Doing other stuff in main thread: " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));

    std::cout << "Getting result..." << std::endl;
    int result = future_result.get(); // 阻塞,直到结果可用
    std::cout << "Result: " << result << std::endl;

    return 0;
}

代码解释:

  1. std::async(std::launch::async, calculate_sum, 10, 20): 这行代码创建了一个task,它会在一个新的线程里执行calculate_sum(10, 20)std::launch::async 表示强制在新线程中执行。
  2. future_result.get(): 这行代码会阻塞,直到calculate_sum执行完毕,并返回结果。如果calculate_sum抛出异常,get()也会抛出同样的异常。

注意事项:

  • std::launch::async:强制在新线程中执行。
  • std::launch::deferred:延迟执行,直到调用get()wait()
  • 如果不指定launch策略,std::async会根据系统资源情况自动选择。

taskget()方法:获取结果,处理异常

task最核心的方法就是get()。它会阻塞,直到异步操作完成,然后返回结果。如果异步操作抛出异常,get()也会抛出同样的异常。

#include <iostream>
#include <future>
#include <stdexcept>

int might_throw() {
    throw std::runtime_error("Something went wrong!");
}

int main() {
    std::future<int> future_result = std::async(std::launch::async, might_throw);

    try {
        int result = future_result.get();
        std::cout << "Result: " << result << std::endl; // 不会执行到这里
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }

    return 0;
}

代码解释:

  1. might_throw(): 这个函数会抛出一个std::runtime_error异常。
  2. future_result.get(): 这行代码会捕获might_throw()抛出的异常,并重新抛出。

重点: task会自动处理异常,避免程序直接崩溃。

taskwait()wait_for()方法:等待任务完成

有时候,我们不需要立即获取结果,只需要等待任务完成即可。这时候,就可以用wait()wait_for()方法。

  • wait(): 阻塞,直到任务完成。
  • wait_for(): 阻塞一段时间,如果任务在这段时间内完成,就返回std::future_status::ready,否则返回std::future_status::timeout
#include <iostream>
#include <future>
#include <chrono>
#include <thread>

int calculate_something() {
    std::this_thread::sleep_for(std::chrono::seconds(3));
    return 42;
}

int main() {
    std::future<int> future_result = std::async(std::launch::async, calculate_something);

    std::cout << "Waiting for the task to complete..." << std::endl;

    if (future_result.wait_for(std::chrono::seconds(2)) == std::future_status::timeout) {
        std::cout << "Task timed out!" << std::endl;
    } else {
        std::cout << "Task completed!" << std::endl;
        int result = future_result.get(); // Now it won't block
        std::cout << "Result: " << result << std::endl;
    }

    return 0;
}

代码解释:

  1. future_result.wait_for(std::chrono::seconds(2)): 这行代码会阻塞最多2秒钟,等待calculate_something()执行完毕。
  2. 如果2秒钟后calculate_something()还没有执行完毕,wait_for()会返回std::future_status::timeout

task的组合:让异步操作像搭积木一样

task最强大的地方在于它可以组合。我们可以把多个task组合起来,形成更复杂的异步流程。

方法一:then()(C++20)

C++20 引入了 then() 方法,让 task 的组合更加优雅。then() 允许你指定一个回调函数,当 task 完成后,回调函数会自动执行。

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

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

int calculate_cube(int x) {
    std::cout << "Calculating cube in thread: " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return x * x * x;
}

int main() {
    std::future<int> future_square = std::async(std::launch::async, calculate_square, 5);

    // C++20's then() requires a bit more setup, often with a shared_future
    auto shared_future_square = future_square.share(); // Convert to shared_future

    auto future_cube = std::async(std::launch::async, [shared_future_square]() {
        int square = shared_future_square.get(); // Get the result of the square calculation
        return calculate_cube(square);
    });

    std::cout << "Getting cube..." << std::endl;
    int cube = future_cube.get();
    std::cout << "Cube: " << cube << std::endl;

    return 0;
}

代码解释:

  1. std::async(std::launch::async, calculate_square, 5): 创建一个 task 计算 5 的平方。
  2. future_square.share(): 将 future_square 转换为 shared_futureshared_future 允许多个线程访问结果。
  3. std::async(std::launch::async, [shared_future_square]() { ... }): 创建第二个 task,它的回调函数会等待 future_square 完成,获取平方的结果,然后计算立方。

方法二:手动组合(适用于 C++11/14/17)

在 C++20 之前,then() 并不存在,我们需要手动组合 task。 这通常涉及到 lambda 表达式和 get() 方法。

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

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

int calculate_cube(int x) {
    std::cout << "Calculating cube in thread: " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return x * x * x;
}

int main() {
    std::future<int> future_square = std::async(std::launch::async, calculate_square, 5);

    std::future<int> future_cube = std::async(std::launch::async, [&future_square]() {
        int square = future_square.get(); // Get the result of the square calculation
        return calculate_cube(square);
    });

    std::cout << "Getting cube..." << std::endl;
    int cube = future_cube.get();
    std::cout << "Cube: " << cube << std::endl;

    return 0;
}

代码解释:

  1. std::async(std::launch::async, calculate_square, 5): 创建一个 task 计算 5 的平方。
  2. std::async(std::launch::async, [&future_square]() { ... }): 创建第二个 task,它的回调函数会等待 future_square 完成,获取平方的结果,然后计算立方。 注意这里使用了 lambda 表达式,并且通过引用捕获了 future_square

shared_future:多个线程共享结果

有时候,我们需要让多个线程共享一个task的结果。这时候,就可以用shared_futureshared_future允许多个线程同时访问同一个task的结果。

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

int calculate_value() {
    std::cout << "Calculating value in thread: " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 100;
}

void process_value(std::shared_future<int> shared_future, int thread_id) {
    std::cout << "Thread " << thread_id << " waiting for result..." << std::endl;
    int value = shared_future.get();
    std::cout << "Thread " << thread_id << " received value: " << value << std::endl;
}

int main() {
    std::future<int> future_value = std::async(std::launch::async, calculate_value);
    std::shared_future<int> shared_future_value = future_value.share();

    std::vector<std::thread> threads;
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back(process_value, shared_future_value, i);
    }

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

    return 0;
}

代码解释:

  1. std::shared_future<int> shared_future_value = future_value.share(): 这行代码把future_value转换成shared_future
  2. process_value(std::shared_future<int> shared_future, int thread_id): 这个函数会等待shared_future的结果,并打印出来。
  3. 多个线程可以同时调用shared_future.get(),而不会出现竞争条件。

task的注意事项和最佳实践

  • 避免死锁: 在使用get()方法时,要特别小心死锁。例如,在一个task的回调函数中调用同一个taskget()方法,可能会导致死锁。
  • 异常处理: 要确保正确处理task中可能抛出的异常。否则,可能会导致程序崩溃。
  • 避免长时间阻塞: 尽量避免在主线程中长时间阻塞。可以使用wait_for()方法设置超时时间。
  • 选择合适的launch策略: 根据实际情况选择合适的launch策略。std::launch::async适用于需要并行执行的任务,std::launch::deferred适用于只需要在需要结果的时候才执行的任务。
  • 使用shared_future共享结果: 当多个线程需要访问同一个task的结果时,使用shared_future

task与其他异步编程技术的比较

特性 task (std::future) std::thread Boost.Asio
抽象级别
异常处理 内置,自动传播 需要手动处理 需要手动处理
结果获取 get() 共享内存/消息队列 回调函数
组合性 C++20 then(),手动组合 困难 复杂
适用场景 简单异步任务,需要结果 细粒度线程控制 网络编程

总结:

  • task 适合于需要获取结果的简单异步任务。
  • std::thread 适合于需要细粒度线程控制的场景。
  • Boost.Asio 适合于网络编程。

总结

task是C++异步编程的利器。它可以统一封装异步操作,简化异常处理,方便组合异步操作。掌握task的用法,可以让你写出更简单、更安全、更可控的异步代码。 希望今天的课程对大家有所帮助! 感谢各位的观看,下次再见!

发表回复

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