C++11并发支持库(condition_variable | future全家桶)

一、std::condition_variable

condition_variable是我们C++封装后的互斥量

condition_variable需要配合互斥锁系列进行使用,主要提供wait和notify系统接口。

wait需要传递⼀个unique_lock类型的互斥锁,wait会阻塞当前线程直到被notify。在进入阻塞的⼀瞬间,会解开互斥锁,方便其他线程获取锁,访问条件变量。当被notify唤醒时,他会 同时获取到锁,再继续往下运行。

notify_one会唤醒当前条件变量上等待的其中⼀个线程,使用时他也需要用互斥锁保护,如果没有现成阻塞等待,他啥事都不做;notify_all会唤醒当前条件变量上等待的所有线程线程。 condition_variable_any 类是std::condition_variable 的泛化。

相对于只在 std::unique_lock 上工作的std::condition_variable,condition_variable_any 能在 任何满足可基本锁定(BasicLockable)要求的锁上工作。

这里重点介绍一下condition_variable的wait()接口:

wait()接口有两种传参方式,例如下图

这里重点讲第二个传参方式的第二个参数

关于第二个参数文档是这样介绍的:

也就是说第二个参数我们可以传递一个返回bool类型的回调函数,该函数仅在pred返回 时阻塞线程;只有当pred变为时,通知信号才能解除线程阻塞。也就是说假如我们在一个循环体中,为了防止里面循环一直执行,但是条件变量又没有被唤醒,导致一直占用资源,我们就可以用wait()接口的第二个参数来阻塞。

下面演示一个经典的问题,两个线程交替打印奇数和偶数

cpp 复制代码
​
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>

using namespace std;

int main()
{
    std::mutex mtx;
    std::condition_variable c;
    int n = 100;
    bool flag = true;
    thread t1([&](){
        int i=0;
        while(i<n)
        {
            unique_lock<mutex> lock(mtx);
            while(!flag)
            {
                c.wait(lock);
            }
            cout<<i<<endl;
            flag = !flag;
            i+=2;
            c.notify_all();
        }
    });
    thread t2([&](){
        int j=1;
        while(j<n)
        {
            unique_lock<mutex> lock(mtx);
            while(flag)
            {
                c.wait(lock);
            }
            cout<<j<<endl;
            flag = !flag;
            j+=2;
            c.notify_all();
        }
    });
    t1.join();
    t2.join();

}

​

二、std::future全家桶介绍

2.1 概览

为什么C++11会有future这套组件呢?

在 C++11 之前,想做异步任务、线程间传值、等待任务完成,只能用裸线程 + 互斥锁 + 条件变量,代码繁琐、容易死锁、传值麻烦。

C++11 推出了一套异步任务标准库

  • 不用手动管理线程、锁、条件变量
  • 安全在线程间传递返回值 / 异常
  • 优雅等待任务完成、查询任务状态

这套标准库的核心就是:std::future 异步结果获取器

核心组件总览

bash 复制代码
组件	                        作用
std::future	            只读的异步结果,只能被一个线程获取
std::shared_future	    可共享的异步结果,多线程都能获取
std::async	            最简单:启动异步任务,直接返回 future
std::launch	            控制 async 的启动策略(同步 / 异步 / 自动)
std::promise	        手动设置值,把值塞给 future
std::packaged_task	    包装函数 / 可调用对象,自动把返回值给 future
std::future_status	    查询 future 任务状态(完成 / 超时 / 未就绪)

一句话总结:async /promise/packaged_task 是 "生产者",负责产生异步结果;future/shared_future 是 "消费者",负责获取结果。

2.2 基础:std::future(异步结果的唯一持有者)

future 是一个模板类 ,用来获取异步任务的结果(返回值 / 异常)。

核心特性

  • 只能移动,不能拷贝(一个结果只能被一个 future 持有)
  • 调用 .get()阻塞直到结果就绪
  • 只能调用一次 .get(),第二次会抛异常

核心方法

cpp 复制代码
// 阻塞等待,获取结果(只能调用一次)
T get();

// 非阻塞查询任务状态
future_status wait_for(时间);
future_status wait_until(时间点);

// 纯阻塞等待,不获取结果
void wait();

// 判断是否有共享状态(是否有效)
bool valid() const;

2.3 std::async

async是最推荐、最简单的异步任务启动方式,不用管线程,一行代码启动异步任务。

要配合future一起使用

sync有两种传参方式:

第一个传参方式是回调函数+形参

第二个传参方式是任务启动策略+回调函数+新参(任务启动策略下一小节讲,所以这里只演示第一种传参方式的使用方法)

样例:

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

using std::cout;
using std::endl;


int calc(int x)
{
	std::this_thread::sleep_for(std::chrono::seconds(2));
	return x * 2;
}

int main()
{
	//1.启动异步任务,返回future
	std::future<int> f = std::async(calc, 10);
	cout << "任务已启动,等待结果..." << endl;

	//2.阻塞等待结果
	int res = f.get();
	cout << "结果:" << res << endl; // 输出 20
	return 0;
}

2.4 任务启动策略std::launch

利用launch我们可以放入async的第一个参数决定任务如何执行:

cpp 复制代码
// 枚举值
launch::async      // 立即创建新线程,异步执行(强制异步)
launch::deferred   // 不创建线程,直到调用 get() 才在当前线程同步执行(懒加载)
launch::async | launch::deferred  // 默认值,系统自动选择

区分launch::async与launch::deferred区别:

launch::async任务策略样例:

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

using std::cout;
using std::endl;


int calc(int x)
{
	cout << "异步任务开始启动" << endl;
	std::this_thread::sleep_for(std::chrono::seconds(2));
	return x * 2;
}
int main()
{
	//1.启动异步任务,返回future
	std::future<int> f = std::async(std::launch::async,calc, 10);
	std::this_thread::sleep_for(std::chrono::seconds(1));
	cout << "任务已启动,等待结果..." << endl;

	//2.阻塞等待结果
	int res = f.get();
	cout << "结果:" << res << endl; // 输出 20
	return 0;
}

输出结果:

cpp 复制代码
异步任务开始启动
任务已启动,等待结果...
结果:20

launch::deferred样例:

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

using std::cout;
using std::endl;


int calc(int x)
{
	cout << "异步任务开始启动" << endl;
	std::this_thread::sleep_for(std::chrono::seconds(2));
	return x * 2;
}
int main()
{
	//1.启动异步任务,返回future
	std::future<int> f = std::async(std::launch::deferred,calc, 10);
	std::this_thread::sleep_for(std::chrono::seconds(3));
	cout << "任务已启动,等待结果..." << endl;

	//2.阻塞等待结果
	int res = f.get();
	cout << "结果:" << res << endl; // 输出 20
	return 0;
}

输出结果:

bash 复制代码
任务已启动,等待结果...
异步任务开始启动
结果:20

2.5 任务状态查询:std::future_status

future_status提供了我们可以非阻塞查询任务是否完成,避免异步任务长,调用线程一直阻塞,等待任务完成。

例如:

cpp 复制代码
future<int> f = async(calc, 10);
int res = f.get(); // 死等,calc 跑多久,这里就卡多久

future_status是枚举,有 3 种状态:

1.ready:任务完成,结果就绪

2.timeout:等待超时,任务未完成

3.deferred:任务是 deferred 类型,未执行

样例:

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

using std::cout;
using std::endl;


int calc(int x)
{
	cout << "异步任务开始启动" << endl;
	std::this_thread::sleep_for(std::chrono::seconds(3));
	return x * 2;
}
int main()
{
	//1.启动异步任务,返回future
	std::future<int> f = std::async(std::launch::async,calc, 10);
	auto status = f.wait_for(std::chrono::seconds(2)); // 最多等2秒

	//2.任务状态
	if (status == std::future_status::ready) {
		cout << "完成:" << f.get() << endl;
	}
	else if (status == std::future_status::timeout) {
		cout << "2秒超时,任务还在跑" << endl;
		// 可以选择继续等,或做别的事
	}
	return 0;
}

注意:我们的wait_for返回值是std::future_status类型,作用:等待一段时间,超时就返回。

wait_until同理,只不过是绝对时间点

样例的输出结果:

cpp 复制代码
异步任务开始启动
2秒超时,任务还在跑

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

future只能移动、只能 get() 一次,多线程都想获取同一个结果时必须用 shared_future。

相比于future不可以拷贝,shared_future是支持拷贝的,且多个线程可以同时get(),但是结果只计算一次,所有线程拿到同一份

我们可以通过future的share()接口得到其shared_future

样例:

cpp 复制代码
#include<iostream>
#include<future>
#include<chrono>
#include<thread>
#include<mutex>   // 必须加

using std::cout;
using std::endl;

std::mutex mtx; // 全局互斥锁,保护cout

int calc(int x)
{
	cout << "异步任务开始启动" << endl;
	std::this_thread::sleep_for(std::chrono::seconds(3));
	return x * 2;
}
int main()
{
	std::future<int> f = std::async(calc, 10);
	std::shared_future<int> sf = f.share();

	auto func = [](std::shared_future<int> sf) {
		std::lock_guard<std::mutex> lock(mtx); // 加锁,保证整行输出不被打断
		cout << "线程: " << std::this_thread::get_id() << "  获取结果:" << sf.get() << endl;
		};

	std::thread t1(func, sf);
	std::thread t2(func, sf);

	t1.join();
	t2.join();
	return 0;
}

适用场景:一个任务,多个消费者线程等待结果。

2.7 手动设置值:std::promise(底层生产者)

promise是手动向 future 塞值 的工具,适合线程间手动通信

执行流程

  1. 创建 promise<T>
  2. 从 promise 获取 future<T>
  3. 线程 A:通过 promise.set_value(...) 设置结果
  4. 线程 B:通过 future.get() 获取结果

样例:

cpp 复制代码
void thread_func(promise<int>& p) {
    this_thread::sleep_for(chrono::seconds(1));
    p.set_value(99); // 手动设置结果
}

int main() {
    promise<int> p;
    future<int> f = p.get_future(); // 绑定 future

    thread t(thread_func, ref(p)); // 启动线程

    cout << "等待结果..." << endl;
    cout << f.get() << endl; // 99

    t.join();
    return 0;
}

2.8 包装函数任务:std::packaged_task

packaged_task = 包装一个函数 / 仿函数 /lambda,自动把返回值 / 异常传给 future。

它是 async底层实现 之一,比 async 更灵活(可以自己控制线程,所以相比与async我们可以与线程池配合使用)。

执行流程

  1. 包装函数 → packaged_task<T(Args...)>
  2. 获取 future
  3. 扔到线程执行
  4. 获取结果
cpp 复制代码
int calc(int a, int b) { return a + b; }

int main() {
    // 包装函数
    packaged_task<int(int, int)> task(calc);
    // 获取 future
    future<int> f = task.get_future();

    // 手动创建线程执行任务
    thread t(move(task), 10, 20);

    //上述代码就等价于async(F f,Args args...)
    cout << f.get() << endl; // 30

    t.join();
    return 0;
}

package_task只支持移动不支持拷贝(大概是因为future只支持一个线程拥有,所以future只支持移动,不支持拷贝,然后package_task又与一个future绑定,所以package_task也只支持移动,不支持拷贝)

2.9 容易犯错的坑

  • async 如果不接收返回值,会同步阻塞

    复制代码
    async(launch::async, calc); // 会阻塞!

    必须用 future 接收:auto f = async(...)

  • future 无效时调用 get/wait 会崩溃 先用 f.valid() 判断。

  • 异常会自动传递 异步任务抛异常,get() 时会在当前线程抛出。

例如抛异常的场景样例:

cpp 复制代码
int div(int a, int b)
{
    if(b == 0)
        throw runtime_error("除数不能为0");
    return a / b;
}

int main()
{
    packaged_task<int(int,int)> task(div);
    future<int> f = task.get_future();

    thread t(move(task), 10, 0);

    try
    {
        cout << f.get() << endl;
    }
    catch (exception& e)
    {
        cout << "捕获异常:" << e.what() << endl;
    }

    t.join();
    return 0;
}

2.10 经典场景C++11实现的线程池

那么看完我之前的C++11并发支持库(1)和(2)之后,我们可以写一个简易的C++11线程池

具体如下:

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
#include <memory>

using std::cout;
using std::endl;


class ThreadPool
{
public:
	explicit ThreadPool(size_t pool_count):
		pool_count_(pool_count),stop_(false)
	{
		for (size_t i = 0; i < pool_count_; ++i)
		{
			workers_.emplace_back([this]() {
				while (true)
				{
					std::function<void()> task;
					{
						std::unique_lock<std::mutex> lock(mtx_);
						// 等待任务或停止信号
						cv_.wait(lock, [this]() {
							return stop_ || !tasks_.empty();
							});
						if (stop_ && tasks_.empty()) return;
						//线程池停止且无任务,退出
						task = std::move(tasks_.front());
						tasks_.pop();
					}
					task();
				}
				});
		}
	}

	template<class F,class... Args>
	auto submit(F&& f, Args&&... args)->std::future<decltype(f(args...))>
	{
		using RetType = decltype(f(args...));
		//用包装器再次封装函数,从而可以得到future类型,调用线程可以调用后,得到执行结果
		auto task = std::make_shared<std::packaged_task<RetType()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
		std::future<RetType> res = task->get_future();
		{
			std::unique_lock<std::mutex> lock(mtx_);
			if (stop_)
				throw std::runtime_error("ThreadPool has stopped");
			tasks_.emplace([task]() { (*task)(); });
		}
		cv_.notify_one();
		return res;
	}

	~ThreadPool()
	{
		{
			std::unique_lock<std::mutex> lock(mtx_);
			stop_ = true;
		}
		cv_.notify_all();
		for (auto& t : workers_)
		{
			t.join();
		}
	}
	//进制拷贝和移动
	ThreadPool(const ThreadPool&) = delete;
	ThreadPool& operator=(const ThreadPool&) = delete;
	ThreadPool(const ThreadPool&&) = delete;
	ThreadPool& operator=(const ThreadPool&&) = delete;

private:
	bool stop_;                                 //
	size_t pool_count_;
	std::vector<std::thread> workers_;           //工作线程
	std::queue<std::function<void()>> tasks_;  //任务队列
	std::mutex mtx_;
	std::condition_variable cv_;

};


int main()
{
	ThreadPool pool(4); // 创建4个线程
	std::mutex mtx;
	// 提交普通任务
	for (int i = 0; i < 8; ++i)
	{
		pool.submit([&mtx,i]()
			{
				{
					std::unique_lock<std::mutex> lock(mtx);
					std::cout << "task " << i
						<< " run in thread: "
						<< std::this_thread::get_id() << std::endl;
				}
				std::this_thread::sleep_for(std::chrono::seconds(1));
			});
	}

	// 提交带返回值任务
	auto f = pool.submit([](int a, int b) {
		std::this_thread::sleep_for(std::chrono::seconds(2));
		return a + b;
		}, 10, 20);

	std::cout << "result: " << f.get() << std::endl;

	return 0;
}
相关推荐
2401_850491651 小时前
使用 curl 调用 Go 标准库 RPC 服务(JSON-RPC 协议详解)
jvm·数据库·python
阿Y加油吧1 小时前
二刷 LeetCode:爬楼梯与杨辉三角,Java 实现复盘
java·算法·leetcode
落羽的落羽1 小时前
【项目】C++从零实现JsonRpc框架——项目引入
linux·服务器·开发语言·c++·人工智能·算法·机器学习
不知名的忻1 小时前
堆排序(Java)
java·数据结构·算法·排序算法
TAN-90°-1 小时前
Java 5——final 抽象 接口
java·开发语言
Andy1 小时前
C++ 容器适配器_栈_队列_双端队列
开发语言·网络·c++
吴声子夜歌1 小时前
Java——显示锁
java·开发语言
CLX05051 小时前
SQL排查JOIN查询中索引失效的常见情况_数据类型隐式转换
jvm·数据库·python
思麟呀1 小时前
在C++基础上理解Csharp-2
开发语言·jvm·c++·c#