「C++黑魔法」future与promise:不加锁的异步编程,原来可以这么简单!

大家好,我是小康。

你是否曾经为了让程序同时做多件事而绞尽脑汁?是否被多线程编程的各种锁、条件变量搞得头昏脑胀?今天,我要告诉你一个秘密武器,让你轻松驾驭异步编程的海洋!

前言:为什么要学future和promise?

朋友,想象一下这个场景:你在餐厅点了一份需要20分钟才能做好的复杂菜品。你有两个选择:

  1. 坐在那里盯着厨房门口,等待20分钟(同步等待)
  2. 服务员给了你个取餐码,菜品好了会通知你,同时你可以刷刷手机或聊聊天(异步等待)

显然,第二种方式更高效,对吧?

在C++编程中,futurepromise就像是这个"取餐码+通知"系统,让你的程序能够优雅地处理异步任务。它们是C++11引入的现代并发编程工具,比传统的线程、互斥锁和条件变量更加简单易用。

一、异步任务是个啥?通俗地说就是"后台运行"

在解释futurepromise之前,我们先聊聊什么是异步任务。

异步任务就是指那些可以在"后台"执行,不需要主线程等待的任务。比如:

  • 下载一个大文件
  • 复杂计算(如图像处理)
  • 访问远程服务器

想象一下你的电脑在下载游戏的同时,你还能继续刷视频、聊天,这就是异步的魅力!

二、future:未来会得到的结果

future可以理解为"未来的结果",它就像一张电影票根:

  • 你现在拿着票根(future)
  • 电影(异步任务)正在后台准备中
  • 当电影准备好了,你可以用票根进场(获取结果)

用代码说话:

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

int compute_answer() {
    // 假装这是个复杂计算
    std::cout << "开始计算终极问题的答案..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
    std::cout << "计算完成!" << std::endl;
    return 42; // 返回结果
}

int main() {
    // 启动异步任务,立即返回一个future
    std::cout << "主线程:启动一个耗时任务" << std::endl;
    std::future<int> answer_future = std::async(compute_answer);

    std::cout << "主线程:哇,不用等待,我可以继续做其他事情!" << std::endl;

    // 做一些其他工作...
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "主线程:我在异步任务计算的同时做了些其他事" << std::endl;

    // 当需要结果时,我们可以获取它
    // 如果结果还没准备好,这会阻塞直到结果可用
    std::cout << "主线程:好了,现在我需要知道答案了,等待结果..." << std::endl;
    int answer = answer_future.get();

    std::cout << "终极答案是:" << answer << std::endl;

    return 0;
}

输出结果

plain 复制代码
主线程:启动一个耗时任务
开始计算终极问题的答案...
主线程:哇,不用等待,我可以继续做其他事情!
主线程:我在异步任务计算的同时做了些其他事
计算完成!
主线程:好了,现在我需要知道答案了,等待结果...
终极答案是:42

看到了吗?主线程启动了计算,但并不立即等待结果,而是继续执行其他代码。只有当真正需要结果时(调用get()),才会等待异步任务完成。

三、promise:我保证会给你结果

如果说future是领取结果的凭证,那么promise就是一个承诺:"我保证会在某个时刻设置一个值"。它们是一对好搭档:

  • promise负责在某个时刻设置结果
  • future负责在需要时获取结果

这就像你和朋友的约定:

  • 你:我承诺会告诉你考试成绩(promise)
  • 朋友:我会等你告诉我(future)

来看个例子:

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

void producer(std::promise<int> my_promise) {
    std::cout << "生产者:我要开始生产一个重要的值了..." << std::endl;

    // 假装我们在做一些复杂的计算
    std::this_thread::sleep_for(std::chrono::seconds(2));

    int result = 42;
    std::cout << "生产者:计算完成,设置结果到promise" << std::endl;

    // 设置promise的值,这会通知相关的future
    my_promise.set_value(result);
}

int main() {
    // 创建一个promise
    std::promise<int> answer_promise;

    // 从promise获取一个future
    std::future<int> answer_future = answer_promise.get_future();

    // 启动一个线程,传入promise
    std::cout << "主线程:启动生产者线程" << std::endl;
    std::thread producer_thread(producer, std::move(answer_promise));

    // 主线程继续做其他事情
    std::cout << "主线程:我可以做自己的事,不用等待..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 当需要结果时
    std::cout << "主线程:现在我需要结果了,等待future..." << std::endl;
    int answer = answer_future.get();
    std::cout << "主线程:收到结果:" << answer << std::endl;

    // 别忘了等待线程结束
    producer_thread.join();

    return 0;
}

输出结果

plain 复制代码
主线程:启动生产者线程
生产者:我要开始生产一个重要的值了...
主线程:我可以做自己的事,不用等待...
主线程:现在我需要结果了,等待future...
生产者:计算完成,设置结果到promise
主线程:收到结果:42

这个例子展示了如何使用promisefuture在线程间传递结果。生产者线程通过promise设置值,主线程通过future获取值。

四、future的几种获取方式

除了通过promise获取future,C++11还提供了其他便捷方式:

1. 通过async获取future

std::async是最简单的方式,它自动创建线程并返回future

cpp 复制代码
std::future<int> result = std::async([]() {
    return 42;
});

2. 通过packaged_task获取future

std::packaged_task包装了一个可调用对象,并允许你获取其future

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

int main() {
    // 创建一个packaged_task,包装一个lambda函数
    std::packaged_task<int(int, int)> task([](int a, int b) {
        std::cout << "计算 " << a << " + " << b << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时计算
        return a + b;
    });

    // 获取future
    std::future<int> result = task.get_future();

    // 在新线程中执行任务
    std::thread task_thread(std::move(task), 10, 32);

    // 主线程做其他事情...
    std::cout << "主线程:等待计算结果..." << std::endl;

    // 获取结果
    int sum = result.get();
    std::cout << "结果是:" << sum << std::endl;

    task_thread.join();
    return 0;
}

输出结果

plain 复制代码
主线程:等待计算结果...
计算 10 + 32
结果是:42

五、实用功能:future的超时等待

有时候,我们不想无限期地等待异步任务。future提供了带超时的等待功能:

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

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

int main() {
    auto future = std::async(std::launch::async, long_calculation);

    // 设置1秒超时
    auto status = future.wait_for(std::chrono::seconds(1));

    if (status == std::future_status::ready) {
        std::cout << "任务已完成,结果是:" << future.get() << std::endl;
    } else if (status == std::future_status::timeout) {
        std::cout << "等待超时!任务还没完成" << std::endl;

        // 我们仍然可以继续等待完成
        std::cout << "继续等待..." << std::endl;
        std::cout << "最终结果:" << future.get() << std::endl;
    }

    return 0;
}

输出结果

plain 复制代码
等待超时!任务还没完成
继续等待...
最终结果:42

六、异常处理:当异步任务出错时

异步任务中的异常会被捕获并存储在future中,当你调用get()时会重新抛出:

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

int may_throw() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    throw std::runtime_error("哎呀,出错了!");
    return 42;
}

int main() {
    auto future = std::async(may_throw);

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

    return 0;
}

输出结果

plain 复制代码
捕获到异常:哎呀,出错了!

这种设计非常优雅------无论异步任务是成功返回值还是抛出异常,都能通过同一个future接口处理。

七、实际应用案例:并行计算求和

让我们用一个更实用的例子来巩固理解:并行计算大数组的和。

cpp 复制代码
#include <iostream>
#include <vector>
#include <numeric>
#include <future>
#include <chrono>

// 计算数组部分和的函数
long long partial_sum(const std::vector<int>& data, size_t start, size_t end) {
    return std::accumulate(data.begin() + start, data.begin() + end, 0LL);
}

int main() {
    // 创建一个大数组
    const size_t size = 100000000; // 1亿个元素
    std::vector<int> data(size, 1); // 全是1的数组

    auto start_time = std::chrono::high_resolution_clock::now();

    // 单线程计算
    long long single_result = std::accumulate(data.begin(), data.end(), 0LL);

    auto single_end = std::chrono::high_resolution_clock::now();
    auto single_duration = std::chrono::duration_cast<std::chrono::milliseconds>(single_end - start_time);

    std::cout << "单线程结果: " << single_result << " (耗时: " 
        << single_duration.count() << "ms)" << std::endl;

    // 使用4个线程并行计算
    auto multi_start = std::chrono::high_resolution_clock::now();

    const size_t num_threads = 4;
    const size_t block_size = size / num_threads;

    std::vector<std::future<long long>> futures;

    for (size_t i = 0; i < num_threads; ++i) {
        size_t start = i * block_size;
        size_t end = (i == num_threads - 1) ? size : (i + 1) * block_size;

        // 启动异步任务
        futures.push_back(std::async(std::launch::async, 
            partial_sum, std::ref(data), start, end));
    }

    // 收集结果
    long long multi_result = 0;
    for (auto& f : futures) {
        multi_result += f.get();
    }

    auto multi_end = std::chrono::high_resolution_clock::now();
    auto multi_duration = std::chrono::duration_cast<std::chrono::milliseconds>(multi_end - multi_start);

    std::cout << "多线程结果: " << multi_result << " (耗时: " 
        << multi_duration.count() << "ms)" << std::endl;

    std::cout << "加速比: " << static_cast<double>(single_duration.count()) / multi_duration.count() << "x" << std::endl;

    return 0;
}

可能的输出结果(取决于你的硬件):

plain 复制代码
单线程结果: 100000000 (耗时: 570ms)
多线程结果: 100000000 (耗时: 171ms)
加速比: 3.33333x

看到没?多线程版本明显更快!这正是future的价值所在------让并行编程变得简单而高效。

八、总结:为什么future和promise这么香?

现在,你已经了解了C++11中futurepromise的基本用法。它们的优势在于:

  1. 简化异步编程:比直接管理线程、互斥锁和条件变量简单得多
  2. 清晰的所有权模型promise负责生产值,future负责消费值
  3. 异常传递 :异步任务中的异常会自动传递给等待的future
  4. 超时控制:可以设置等待超时,避免无限阻塞
  5. 与现代C++完美融合:配合lambda、智能指针等现代特性使用更加优雅

记住这个类比就行:promise就像一个"承诺给你结果的人",future就像"等待结果的凭证"。

下次当你需要在程序中执行耗时操作又不想阻塞主线程时,就想到futurepromise吧!它们会让你的代码更加现代、高效,还能充分利用多核处理器的威力。

最后一个小提示:虽然C++11的futurepromise已经很强大,但如果你追求更高级的异步编程,可以考虑看看C++20的协程(coroutine)特性,那又是另一个让人兴奋的话题了~


写在最后:这只是异步编程旅程的开始...

嘿,朋友!看到这里,你已经掌握了C++异步编程的一把利器了!但编程世界就像宇宙一样浩瀚无边,我们的探索才刚刚开始~

学到了新知识?有疑问?有心得?

👉 快到评论区和大家分享吧!我会亲自解答你的每一个问题。

如果这篇文章让你有所收获...

请点赞、收藏、关注哦,这对我来说,就像异步任务完成时收到的一个promise.set_value()一样珍贵!转发给你的程序猿/媛朋友,他们一定也会感谢你分享这个编程"黑魔法"!

想要更多编程"神器"吗?

关注我的公众号「跟着小康学编程」,这里有:

🚀 多线程编程实战秘籍 ------ 让你的程序飞起来,CPU核心全利用!

🔍 C++面试题深度解析 ------ 面试官:这水平,必须给offer!

🛠️ 现代C++高级技巧 ------ C++11/14/17/20新特性全解析,代码立减50%!

🧩 STL源码探秘 ------ 看大神是怎么写代码的,学就要学最好的!

💻 实战项目分享 ------ 纸上得来终觉浅,实战出真知!

👇 扫码即可关注

哈,想不想和一群志同道合的C++爱好者一起交流?

扫描下方二维码,加入我们的技术交流群!

群里有什么?有大厂C++开发工程师坐镇,有热心的同行互帮互助...

遇到 bug 卡住了?代码性能优化不上去?面试准备没方向?

别着急,一个消息发出去,群里的大佬们立马帮你解决!

记住,在编程的世界里,你从不孤单。我们的future已经连接,就等你的get()来获取知识的果实了!