函数式并发:std::async、std::future、Lambda与异步任务

并发编程,说白了就是让程序同时干多件事。传统C++里,你得手动创建线程、管理锁、处理条件变量……写起来又臭又长。但有了Lambda和std::async,事情就简单多了。

我个人习惯把这种模式叫做「扔出去,等结果」。你定义一个任务,把它扔给后台线程,然后该干嘛干嘛。等需要结果了,再伸手去拿。整个过程就像点外卖——你下单,然后继续工作,外卖到了再吃。

std::async:最简单的异步启动方式

std::async是C++11引入的工具。它接受一个可调用对象(Lambda、函数指针、仿函数都行),然后异步执行它。返回值是一个std::future对象,用来获取结果。

看个最简单的例子:

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

int main() {
    // 异步执行一个Lambda
    std::future<int> result = std::async(std::launch::async, []() {
        std::this_thread::sleep_for(std::chrono::seconds(2));
        return 42;
    });

    std::cout << "正在等待结果..." << std::endl;
    
    // 获取结果(会阻塞直到任务完成)
    int value = result.get();
    std::cout << "结果是: " << value << std::endl;

    return 0;
}

这里有个细节:std::launch::async表示强制在新线程上执行。还有另一个策略std::launch::deferred,它不会创建新线程,而是等到调用get()时才在当前线程执行。嗯,这个要区分清楚。

注意:std::future::get()只能调用一次。第二次调用会抛出std::future_error异常。如果你需要多次获取结果,考虑用std::shared_future

std::future:异步结果的占位符

std::future本质上是一个「未来才会有的值」。它提供了几个关键操作:

  • get():阻塞等待结果,并返回
  • wait():只等待,不获取结果
  • wait_for():等待一段时间,超时返回状态
  • wait_until():等待到某个时间点

我在项目中遇到过一个问题:用wait_for轮询多个future时,如果超时时间设得太短,CPU占用会飙升。后来我改用wait_until配合一个合理的截止时间,才解决。

看一个带超时处理的例子:

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

int main() {
    auto future = std::async(std::launch::async, []() {
        std::this_thread::sleep_for(std::chrono::seconds(5));
        return 100;
    });

    // 最多等3秒
    auto status = future.wait_for(std::chrono::seconds(3));
    
    if (status == std::future_status::ready) {
        std::cout << "结果: " << future.get() << std::endl;
    } else if (status == std::future_status::timeout) {
        std::cout << "超时了,任务还没完成" << std::endl;
    } else {
        std::cout << "任务被延迟执行了" << std::endl;
    }

    return 0;
}

Lambda与异步任务的完美结合

Lambda表达式在异步编程中简直是天作之合。你想想看,传统方式你得定义一个函数,或者写一个仿函数类,代码分散在各处。而Lambda可以就地定义,捕获上下文变量,干净利落。

我建议这样组织异步任务:

#include <iostream>
#include <future>
#include <vector>
#include <numeric>

int main() {
    std::vector<int> data(1000000);
    std::iota(data.begin(), data.end(), 1);

    // 把大任务拆成两半,异步执行
    auto mid = data.size() / 2;

    auto future1 = std::async(std::launch::async, [&data, mid]() {
        return std::accumulate(data.begin(), data.begin() + mid, 0LL);
    });

    auto future2 = std::async(std::launch::async, [&data, mid]() {
        return std::accumulate(data.begin() + mid, data.end(), 0LL);
    });

    // 合并结果
    long long sum = future1.get() + future2.get();
    std::cout << "总和: " << sum << std::endl;

    return 0;
}

这里要注意:Lambda捕获的是引用,所以data的生命周期必须长于异步任务。我曾经踩过这个坑——在函数返回后,异步任务还在访问局部变量,结果就是未定义行为。

小技巧:如果异步任务需要拷贝数据,可以用值捕获。或者用std::make_shared创建共享数据,让Lambda捕获智能指针。

异常处理与异步任务

异步任务中抛出的异常,会在调用get()时重新抛出。这意味着你可以在主线程中统一处理异常,而不需要在子线程里处理。

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

int main() {
    auto future = std::async(std::launch::async, []() {
        throw std::runtime_error("任务执行出错");
        return 0;
    });

    try {
        int result = future.get();
        std::cout << "结果: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cout << "捕获到异常: " << e.what() << std::endl;
    }

    return 0;
}

嗯,这里要提醒一下:如果get()没有被调用,异常会被忽略。所以一定要确保每个future最终都被get()wait()处理过。

std::shared_future:多线程共享结果

有时候,一个异步任务的结果需要被多个线程使用。这时候std::future就不够用了——它只能被get()一次。解决方案是std::shared_future

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

int main() {
    // 创建一个promise,手动设置值
    std::promise<int> prom;
    std::shared_future<int> shared_future = prom.get_future().share();

    // 启动多个线程等待结果
    std::vector<std::thread> threads;
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back([shared_future, i]() {
            int value = shared_future.get();
            std::cout << "线程 " << i << " 获取到: " << value << std::endl;
        });
    }

    // 主线程设置结果
    std::this_thread::sleep_for(std::chrono::seconds(1));
    prom.set_value(42);

    // 等待所有线程结束
    for (auto& t : threads) {
        t.join();
    }

    return 0;
}

这个模式在「发布-订阅」场景中特别有用。一个生产者产生数据,多个消费者同时处理。

知识体系总览

下面这张图总结了本章的核心概念和它们之间的关系:

函数式并发知识体系 std::async std::launch::async std::launch::deferred 默认策略(自动选择) std::future<T> get() 获取结果 wait() 等待完成 wait_for() 超时等待 share() 转为shared 核心思想:将任务与执行分离,通过future获取异步结果

最佳实践与避坑指南

最后,分享几个我在实际项目中积累的经验:

  • 不要滥用异步:如果任务本身很轻量(比如几微秒就完成),创建线程的开销反而更大。我一般只对耗时超过1毫秒的任务使用std::async
  • 注意资源限制:同时启动太多异步任务会导致线程爆炸。可以用线程池来限制并发数。
  • 避免死锁:如果异步任务内部又调用了get()等待另一个future,可能会形成死锁。我曾经在一个回调链里遇到过这个问题,排查了半天。
  • 善用Lambda捕获:值捕获适合小数据,引用捕获适合大数据但要注意生命周期。移动捕获(C++14)适合转移所有权。
核心要点:std::async + Lambda 是C++函数式并发编程的黄金组合。它让你用声明式的方式描述异步任务,而不是手动管理线程。记住:任务描述与执行分离,结果通过future获取,异常通过get()传播。

好了,这一章的内容就到这里。函数式并发其实没那么复杂,关键是理解「异步任务」这个抽象概念。你只要把任务扔出去,然后等结果回来就行——剩下的交给标准库去处理。


公众号:蓝海资料掘金营,微信deep3321