C++笔记---并发支持库(future)

1. 异步编程

std::futurestd::async 是 C++11 异步编程的核心组件,定义在<future>头文件中:

  • std::future异步操作结果 的 "句柄",用于在未来获取异步任务的返回值或异常
  • std::async 是异步任务启动器,简化了异步任务的创建,无需手动管理线程和结果传递,直接返回 std::future。

两者结合可高效实现 "启动任务→主线程继续工作→按需获取结果" 的异步编程模式,替代了传统手动创建线程 + 全局变量 / 回调函数的繁琐方式。

2. std::async

std::async函数 模板,用于异步启动一个可调用对象 (函数、lambda、仿函数等),并返回关联的 std::future,简化了 "线程创建 + 结果传递" 的流程。

cpp 复制代码
// 显式指定启动策略
template <class Fn, class... Args>
std::future<typename std::result_of<Fn(Args...)>::type>
async(std::launch policy, Fn&& fn, Args&&... args);

// 默认策略(由实现决定)
template <class Fn, class... Args>
std::future<typename std::result_of<Fn(Args...)>::type>
async(Fn&& fn, Args&&... args);

std::async 的参数与 std::thread 构造函数的参数十分相似,但是多了一个代表异步策略的参数。

异步启动可调用对象有两种策略,由 policy 参数决定:

|---------------------------|-----------------------------------------------------------------------------------------------|
| 策略 | 行为 |
| std::launch::async | 强制异步:立即创建新线程执行任务,任务在独立线程中运行。 |
| std::launch::deferred | 延迟执行:任务不会立即启动,直到调用 future::get()/future::wait() 时,才在当前线程中执行(无新线程);若永远不调用 get()/wait(),任务永不执行。 |
| 默认策略(不传递参数) | 实现可选择两种策略之一(通常根据系统资源动态决定,如线程池负载)。 |

例如:

cpp 复制代码
#include <thread>
#include <iostream>
#include <future>
using namespace std;

void func(const string& name)
{
	cout << name << "正在执行..." << endl;
}

int main()
{
	// 启动一个线程异步执行func
	auto f1 = async(launch::async, func, "异步任务");
	// 延迟执行func
	auto f2 = async(launch::deferred, func, "延迟任务");

	cout << "主线程任务执行中..." << endl;
	this_thread::sleep_for(chrono::seconds(3));
	cout << "主线程任务执行完毕,开始获取异步任务与延迟任务的返回值" << endl;

	// 阻塞等待异步任务执行完成,获得结果,相当于pthread中的join
	f1.get();
	// 在主线程中立即执行延迟任务,获得结果
	f2.get();

	return 0;
}

结果如下:

3. std::future

std::future<T>模板类 (T 为异步任务的返回类型,若任务无返回值则用 std::future<void>),表示一个尚未完成的异步操作的结果

主要成员函数:

|-------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| 函数 | 功能 |
| T get() | (1)获取异步任务结果(阻塞直到完成); (2)任务抛异常时,get() 会重新抛出该异常; (3)仅可调用一次(结果以移动的方式返回)。 |
| bool valid() const | 检查 future 是否关联到有效的异步操作(默认构造 /move 后 /get() 后均为 false)。建议在调用get之前先调用该函数检验。 |
| void wait() | 阻塞当前线程,直到异步任务完成(仅等待,不获取结果)。 |
| future_status wait_for(const chrono::duration& rel_time) | 阻塞指定时长,返回状态: - future_status::ready :任务完成; -future_status::timeout :超时,任务未完成; - future_status::deferred:任务被延迟执行(未启动)。 |
| future_status wait_until(const chrono::time_point& abs_time) | 阻塞直到指定时间点,返回值同 wait_for。 |
| void swap(future& other) | 交换两个 future 的状态。 |
| future(future&& other) | 移动构造(future 不可拷贝,仅可移动)。 |

注意事项:

  • 一个future对象的 get() 只能调用一次,调用后 future 变为无效状态(valid() 返回 false);

  • 若异步任务未完成,调用 get()/wait() 会阻塞当前线程,直到结果可用;

  • 异步任务抛出的异常会被捕获并存储 ,调用 get() 时重新抛出。例如:

    cpp 复制代码
    int risky_task() {
        throw std::runtime_error("异步任务出错!");
        return 42;
    }
    
    int main() {
        std::future<int> fut = std::async(risky_task);
    
        try {
            int res = fut.get(); // 重新抛出任务中的异常
            std::cout << "结果:" << res << std::endl;
        } catch (const std::runtime_error& e) {
            std::cout << "捕获异常:" << e.what() << std::endl; // 输出:异步任务出错!
        }
    
        return 0;
    }
  • future对象的析构函数会阻塞等待异步任务完成 。若临时的future对象没有被变量接收的话,就会导致主线程在该行阻塞等待异步任务完成。

    cpp 复制代码
    void long_task() {
        std::this_thread::sleep_for(std::chrono::seconds(5));
        std::cout << "异步任务完成" << std::endl;
    }
    
    int main() {
        std::async(std::launch::async, long_task); // 阻塞等待5秒
    
        // 其他任务
        // ...
    
        return 0;
    }

若需多次获取异步结果 (如多线程共享结果),可使用 std::shared_future

  • 可拷贝,get() 可调用多次;

  • 可通过 std::future::share() 或移动构造得到:

    cpp 复制代码
    std::future<int> fut = std::async([](){ return 42; });
    std::shared_future<int> sfut = fut.share(); // fut 变为无效
    // 多线程可同时调用 sfut.get()

4. std::future 的其他数据源

std::future 的结果不仅可来自 std::async,还可通过 std::promise 或 std::packaged_task 手动关联,std::async 本质是对这两者的封装:

4.1 std::promise

std::promise 封装了 std::future。我们可以通过get_future 获取 promise 对象内部的 future 对象 ,同时也可以使用其他成员函数对 future 对象写入值或异常

这些写入行为都会导致 future 对象就绪。

|----------------------------------|--------------------------------------|
| 函数 | 功能 |
| get_future | 返回与 promise 对象关联的 future 对象(仅可调用一次)。 |
| set_value | 向 future 对象写入值。 |
| set_exception | 向 future 对象写入异常。 |
| set_value_at_thread_exit | 在线程退出时向 future 对象写入值。 |
| set_exception_at_thread_exit | 在线程退出时向 future 对象写入异常。 |

例如:

cpp 复制代码
std::promise<int> prom;
std::future<int> fut = prom.get_future();

// 新线程中设置值
std::thread t([&prom]() {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    prom.set_value(100); // 设置结果,future 可获取
});

std::cout << "promise 结果:" << fut.get() << std::endl; // 输出 100
t.join();

4.2 std::packaged_task

std::packaged_task 是 C++11 并发库中封装可调用对象并关联异步结果的模板类,核心作用是将 "可调用对象(函数、lambda、仿函数等)" 与 std::future 绑定。

相比于std::function,std::packaged_task对象在被调用后不会直接返回可调用对象的结果(或抛异常),而是将结果(或异常)保存到与其关联的future对象当中

与promise相同,可通过get_future接口获取与其关联的future对象。

主要成员函数:

|--------------------------------------------------|---------------------------------------------------------------------------------------------------------|
| 函数 | 功能 |
| std::future<Ret> get_future() | 获取与当前 packaged_task 关联的 future; 仅可调用一次,二次调用抛出 std::future_error。 |
| void operator()(Args... args) | 执行包装的可调用对象,传入参数 args; 执行结果会自动设置到关联的 future(正常结果 / 异常); 仅可调用一次(除非调用 reset() 重置),重复调用抛 std::future_error。 |
| void make_ready_at_thread_exit(Args... args) | 延迟执行并设置结果: 1. 立即执行包装的任务; 2. 线程退出时才将结果写入 future; 避免 "结果写入后,局部变量析构" 导致的悬空引用。 |
| void reset() | 重置 packaged_task 状态: 1. 放弃当前未完成的结果; 2. 重新绑定可调用对象(保留原对象); 重置后可再次调用 operator() 和 get_future()。 |
| bool valid() const | 检查 packaged_task 是否关联有效的可调用对象(默认构造 /move 后 /reset 前可能为 false)。 |

我们可以使用packaged_task来实现线程池,下面是核心原理的极简示例:

cpp 复制代码
#include <thread>
#include <iostream>
#include <future>
#include <queue>
using namespace std;

template <typename T>
void taskRunner(queue<packaged_task<T>>& taskQueue, mutex& queueMtx, condition_variable& cv)
{
	while (true)
	{
		// 获取一个任务
		packaged_task<T> task;
		{
			unique_lock<mutex> lock(queueMtx);
			while (taskQueue.empty())
			{
				cv.wait(lock);
			}
			task = move(taskQueue.front());
			taskQueue.pop();
		}

		// 空任务表示退出
		if (!task.valid())
			break;

		// 执行任务
		task();
	}
}

int main()
{
	queue<packaged_task<int()>> taskQueue;
	queue<future<int>> resQueue;
	mutex queueMtx;
	condition_variable cv;

	// 启动任务执行线程
	thread worker(taskRunner<int()>, ref(taskQueue), ref(queueMtx), ref(cv));

	// 生成并插入任务
	for (int i = 0; i < 5; i++)
	{
		packaged_task<int()> task([i]() { return i * i; });
		// 保存结果
		resQueue.emplace(task.get_future());

		// 插入任务
		{
			lock_guard<mutex> lock(queueMtx);
			taskQueue.emplace(move(task));
		}

		// 唤醒任务执行线程
		cv.notify_one();
	}

	// 插入空任务表示结束
	{
		lock_guard<mutex> lock(queueMtx);
		taskQueue.emplace();
		cv.notify_one();
	}

	worker.join();

	// 打印结果
	while (!resQueue.empty())
	{
		cout << "执行结果: " << resQueue.front().get() << endl;
		resQueue.pop();
	}

	return 0;
}
相关推荐
全靠bug跑2 小时前
Sentinel 服务保护实战:限流、隔离与熔断降级详解
java·sentinel
五岳2 小时前
Web层接口通用鉴权注解实践(基于JDK8)
java
咖啡の猫2 小时前
Python字典元素的增、删、改操作
java·开发语言·python
PyGata2 小时前
CMake学习笔记(二):CMake拷贝文件夹
c++·笔记·学习
Lucky小小吴2 小时前
JAVA漫谈反序列化篇——笔记
java·开发语言·笔记
摇滚侠2 小时前
Redis 零基础到进阶,Redis 事务,Redis 管道,Redis 发布订阅,笔记47-54
数据库·redis·笔记
仰泳的熊猫2 小时前
1150 Travelling Salesman Problem
数据结构·c++·算法·pat考试
小蜗笔记2 小时前
ABM模型库的笔记
笔记
悠哉悠哉愿意2 小时前
【嵌入式学习笔记】从单片机到嵌入式过渡
笔记·单片机·嵌入式硬件·学习