现代C++:C++11并发支持库

现代C++:C++11并发支持库

一.thread库

thread库文档1thread库文档2

构造函数

c++11的thread一共有4个构造函数:

cpp 复制代码
default (1)	
thread() noexcept;

initialization (2)	
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

copy [deleted] (3)	
thread (const thread&) = delete;

move (4)	
thread (thread&& x) noexcept;

默认空构造,最常用的带函数构造,和移动构造。注意线程是无法被拷贝的(从上面其拷贝构造函数为delete也能看的出来)。它的thread其实本质上就是linux或windows上那一套多线程的封装。

我们来看一个简单的例子:

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

void Print(int i)
{
	for (int j = 0; j < i; j++)
	{
		cout << this_thread::get_id() << " : " << j << endl;
	}
	cout << "\n";
}


int main()
{
	thread t1(Print, 10);
	thread t2(Print, 20);
	t1.join();
	t2.join();
	return 0;
}

输出结果如下:

这也是我们之前遇到过的经典问题:线程饥饿与临界资源没有被保护两个问题。后面我们结合锁与条件变量再进行演示。其他的函数也不需要多说,其实就是我们之前说的linux上的那一套多线程套了一层壳而已,读者可自行查询文档进行使用即可。

二.this_thread

this_thread

this_thread是⼀个命名空间,主要封装了线程相关的4个全局接口函数:

  • get_id是当前执⾏线程的线程id。
  • yield是主动让出当前线程的执⾏权,让其他线程先执⾏。此函数的确切⾏为依赖于实现,特别是取决于使⽤中的 OS 调度器机制和系统状态。例如,先进先出实时调度器(Linux 的 SCHED_FIFO)会挂起当前线程并将它放到准备运⾏的同优先级线程的队列尾,⽽若⽆其他线程在同优先级,则 yield ⽆效果。
  • sleep_for阻塞当前线程执⾏,⾄少 经过指定的 sleep_duration。因为调度或资源争议延迟,此函数可能阻塞⻓于 sleep_duration。
  • sleep_until阻塞当前线程的执⾏,直⾄抵达指定的 sleep_time。函数可能会因为调度或资源纠纷延迟⽽阻塞到 sleep_time 之后的某个时间点。

sleep_for

get_id上面我们已经看过了。yield平时我们也不常用这里了解即可。我们主要看下后面两个函数:

我们来看一个sleep_for的一个例子,比如我想要去实现一个10s的倒计时:

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


int main()
{
	for (int i = 10; i >= 1; i--)
	{
		cout << i << endl;
		this_thread::sleep_for(std::chrono::seconds(1));
	}
	return 0;
}

这里就又谈到了一个新类型,std::chrono。
chrono chrono是⼀个计时相关的类型。
duration 是⽤来管理⼀个相对时间段的类。
time_point 是⽤来管理⼀个绝对时间点的类。

我们简单的来了解下duration这个类型,比如我们想要定义一个1分钟的时间段,可以这样定义:

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


int main()
{
	std::chrono::minutes one_minute1(1);
	std::chrono::seconds one_minute2(60);
	std::chrono::duration<int, std::ratio<60>> one_minute3(1);
	cout << std::chrono::duration_cast<std::chrono::seconds>(one_minute1).count() << "s" << endl;
	cout << std::chrono::duration_cast<std::chrono::seconds>(one_minute2).count() << "s" << endl;
	cout << std::chrono::duration_cast<std::chrono::seconds>(one_minute3).count() << "s" << endl;
	return 0;
}

打印出来的结果都是60s。对于第三种构造方式我们来看下面两个例子:

cpp 复制代码
std::chrono::duration<int, std::ratio<5>> d;  // 每个时间单位 = 5秒
std::chrono::duration<int, std::ratio<1, 2>> d2;  // 每个时间单位 = 0.5秒

std::ratio是c++中用来表示分数的一个类型,像std::ratio<1,2>在数学中就是1/2。

而std::duration的第一个参数则表示了其count返回的值类型,比如:

cpp 复制代码
std::chrono::duration<int, std::ratio<5>> d;
auto d1 = d.count();//d1是一个int类型
std::chrono::duration<double, std::ratio<5>> d2;
auto d3 = d2.count();//d3是一个double类型

所以sleep_for就是让当前线程等待传入的指定时间,但是实际上因为线程等待完毕指定时间之后,其他线程可能在执行任务,不可能说把其他线程停了让此线程执行任务,所以实际上等待的时间要长于传入的指定时间,下面的sleep_until也是一样。

sleep_until

它需要我们传入一个时间戳,然后线程会等到系统时间到达指定时间戳位置之后再进行执行,我们来看一个简单的例子,比如我想要当前线程到从程序启动时刻开始的10s后再执行,可以这样写:

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

std::string time_point_to_string_thread_safe(
    const std::chrono::system_clock::time_point& tp) {
    auto time = std::chrono::system_clock::to_time_t(tp);
    std::tm tm;
    localtime_s(&tm, &time); 
    std::ostringstream oss;
    oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
    return oss.str();
}

int main()
{
	using my_time = std::chrono::duration<int, std::ratio<10>>;
	auto target_time = std::chrono::system_clock::now() + my_time(1);
	std::this_thread::sleep_until(target_time);
	std::cout << "当前时间为:" << time_point_to_string_thread_safe(target_time) << ",程序等待完毕,开始执行任务" << std::endl;
	return 0;
}

效果如下:

三.mutex

文档地址

我们先来看一个例子:

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

int x = 0;
std::mutex mtx;

void Print(int i)
{
	for (int j = 0; j < i; j++)
	{
		++x;
	}
}

int main()
{
	std::thread t1(Print, 100000);
	std::thread t2(Print, 200000);

	t1.join();
	t2.join();
	std::cout << x << std::endl;
	return 0;
}

因为线程竞争等一系列问题最终导致数据少加了不少,所以此时就需要锁来进行解决:

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

int x = 0;
std::mutex mtx;

void Print(int i)
{
	mtx.lock();
	for (int j = 0; j < i; j++)
	{
		++x;
	}
	mtx.unlock();
}

int main()
{
	std::thread t1(Print, 100000);
	std::thread t2(Print, 200000);

	t1.join();
	t2.join();
	std::cout << x << std::endl;
	return 0;
}

当然锁的加锁解锁过程放循环内部肯定要慢于放到循环外部,这点也不再多说。

线程的引用传值问题

假设我们现在有这样一个例子:

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

void Print(int i,int& x,std::mutex& mtx)
{
	mtx.lock();
	for (int j = 0; j < i; j++)
	{
		++x;
	}
	mtx.unlock();
}

int main()
{
	int x = 0;
	std::mutex mtx;
	std::thread t1(Print, 100000,x,mtx);
	std::thread t2(Print, 200000,x,mtx);

	t1.join();
	t2.join();
	std::cout << x << std::endl;
	return 0;
}

这样运行会直接报错。

我们来看下他的底层实现的一段代码:

cpp 复制代码
    template <class _Fn, class... _Args>
    void _Start(_Fn&& _Fx, _Args&&... _Ax) {
        using _Tuple                 = tuple<decay_t<_Fn>, decay_t<_Args>...>;
        auto _Decay_copied           = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
        constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{});

#pragma warning(push)
#pragma warning(disable : 5039) // pointer or reference to potentially throwing function passed to
                                // extern C function under -EHc. Undefined behavior may occur
                                // if this function throws an exception. (/Wall)
        _Thr._Hnd =
            reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));
#pragma warning(pop)

这是vs2019下关于thread构造实现的一小部分代码,我们可以看出来他这边使用了一个_Tuple(后面会细说),因为他底层还是调用的windows/linux下那一套线程库,所以他最终需要将所有参数打包为一个参数包作为参数传入到目标线程所需要执行的函数。

简单些来说,在打包为参数包的这个过程中,他使用了一个完美转发确保原来传入的参数类型不被改变,那么我们再来看上面示例中的代码:

cpp 复制代码
	std::thread t1(Print, 100000,x,mtx);
	std::thread t2(Print, 200000,x,mtx);

他这里实际上我底层打包为参数包时认为他是一个(对于后两个参数)int类型与std::mutex,所以他会进行拷贝构造,最终线程的函数引用的是参数包解析出来的参数而并非我们主线程中的x与mtx。(当然mtx本身也不允许拷贝,这也会是一个错误原因)。

所以此时我们需要将代码进行如下调整才能解决这个问题:

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

void Print(int i,int& x,std::mutex& mtx)
{
	mtx.lock();
	for (int j = 0; j < i; j++)
	{
		++x;
	}
	mtx.unlock();
}

int main()
{
	int x = 0;
	std::mutex mtx;
	std::thread t1(Print, 100000,std::ref(x),std::ref(mtx));
	std::thread t2(Print, 200000, std::ref(x), std::ref(mtx));

	t1.join();
	t2.join();
	std::cout << x << std::endl;
	return 0;
}

std::ref其实是表明我此处传入的是一个引用,我们来看文档网站的一个样例:
std::ref

cpp 复制代码
// ref example
#include <iostream>     // std::cout
#include <functional>   // std::ref

int main () {
  int foo (10);

  auto bar = std::ref(foo);

  ++bar;

  std::cout << foo << '\n';

  return 0;
}

OutPut:11

也就是说auto 解析出来的bar的类型为int&,这样一来对于我们上面说的那个例子就可以让其底层认为我传入的参数类型为int&与std::mutex&从而不再发生拷贝构造。

如果我们不想使用ref,也可以使用lamada表达式进行参数传递:

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

int main()
{
	int x = 0;
	std::mutex mtx;
	auto Print = [&x, &mtx](int i)
	{
		mtx.lock();
		for (int j = 0; j < i; j++)
		{
			++x;
		}
		mtx.unlock();
	};
	std::thread t1(Print, 100000);
	std::thread t2(Print, 200000);

	t1.join();
	t2.join();
	std::cout << x << std::endl;
	return 0;
}

此时他底层会去走另一套构造函数机制(因为lamada表达式本质上是一个仿函数,是一个对象而不是函数指针)。便不需要我们麻烦的去使用ref进行问题解决了。

time_mutex

timed_mutex主要比mutex多了两个函数:

二者其实我们一看就明白他的意思:在一段时间内尝试解锁|到达指定时间戳的时间段内尝试解锁。我们以time_lock_for为例子看下他的用法:

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

void Print(int i,int& x, std::timed_mutex& mtx)
{
	while (!mtx.try_lock_for(std::chrono::seconds(1)))
	{
		std::cout << "-";
	}
	std::cout << i;

	std::this_thread::sleep_for(std::chrono::seconds(5));
	std::cout << "*\n";
	mtx.unlock();
}

int main()
{
	int x = 0;
	std::timed_mutex mtx;
	std::thread t1(Print, 1,std::ref(x),std::ref(mtx));
	std::thread t2(Print, 2, std::ref(x), std::ref(mtx));

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

他的获取锁过程可以这么来概括:初次调用时会进行锁的获取,如果获取到锁了返回true。没有获取锁时不返回false而是进入等待,此过程中如果有线程调用了unlock,阻塞线程便会被唤醒去尝试进行锁的获取,此时也是获取到锁直接返回true向下执行,获取不到继续等待。当等待到给定的时间之后,此时阻塞线程会再进行锁的获取,如果此时获取到就返回true向下执行,获取不到则直接返回false同时阻塞线程退出阻塞状态,表示获取锁超时。

std::recursive_mutex

递归锁顾名思义就是递归时使用的锁,我们看下面的一个例子:

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

void Print(int i,int& x, std::mutex& mtx)
{
	mtx.lock();
	if (x >= 10) {
		mtx.unlock();
		return;
	}
	++x;
	Print(i, x, mtx);
	mtx.unlock();
}

int main()
{
	int x = 0;
	std::mutex mtx;
	std::thread t1(Print, 1,std::ref(x),std::ref(mtx));
	std::thread t2(Print, 2, std::ref(x), std::ref(mtx));
	
	t1.join();
	t2.join();
	std::cout << x << "\n";
	return 0;
}

显然这会引发死锁问题,我们换成recursive_mutex即可解决:

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

void Print(int i,int& x, std::recursive_mutex& mtx)
{
	mtx.lock();
	if (x >= 10) {
		mtx.unlock();
		return;
	}
	++x;
	Print(i, x, mtx);
	mtx.unlock();
}

int main()
{
	int x = 0;
	std::recursive_mutex mtx;
	std::thread t1(Print, 1,std::ref(x),std::ref(mtx));
	std::thread t2(Print, 2, std::ref(x), std::ref(mtx));
	
	t1.join();
	t2.join();
	std::cout << x << "\n";
	return 0;
}

四.lock_guard与unique_lock

lock_guard

lock_guard

锁门卫其实大家也不会太陌生,它本质上就是说防止程序在没有调用unlock之前抛异常退出导致没有解锁或者帮助我们不需要手动的解锁加锁(通常的使用场景下)提供的一种方法,它的使用方式也很简单:

cpp 复制代码
#include <thread>
#include <iostream>
#include <chrono>
#include <sstream>
#include <iomanip>
#include <mutex>
using namespace std;

std::mutex mtx;
int x = 0;

void addX(int i)
{
	lock_guard<std::mutex> lockGuard(mtx);
	for (int j = 0; j < i; j++)
	{
		++x;
	}
}

int main()
{
	thread t1(addX, 10);
	thread t2(addX, 20);
	t1.join();
	t2.join();
	std::cout << x << "\n";
	return 0;
}

调用时自动加锁,出作用域时自动解锁。

当然如果我们在lock_guard之前已经对mtx进行了加锁,但是我们又想让lock_guard来接管后续锁的解锁,可以给它传一个adopt_lock的标识符选项:

cpp 复制代码
#include <thread>
#include <iostream>
#include <chrono>
#include <sstream>
#include <iomanip>
#include <mutex>
using namespace std;

std::mutex mtx;
int x = 0;

void addX(int i)
{
	mtx.lock();
	lock_guard<std::mutex> lockGuard(mtx,adopt_lock);
	for (int j = 0; j < i; j++)
	{
		++x;
	}
}

int main()
{
	thread t1(addX, 10);
	thread t2(addX, 20);
	t1.join();
	t2.join();
	std::cout << x << "\n";
	return 0;
}

unique_lock

unique_lock

它其实也类似与我们上面所说的lock_guard,但是它比lock_guard的功能更加丰富。首先它的构造函数中就有三个标识符选项可以传入:

默认不传任何标识符选项时,他与lock_guard的行为大差不差,adopt_lock与具有adopt_lock标识符选项的lock_guard功能也大差不差。

对于defer_lock,defer具有延迟推迟的意思。它比较适合这样的场景:我想要把一个锁交给unique_lock进行管理,但是我想在交给它管理之后再去进行锁的加锁,此时就可以使用defer_lock:

cpp 复制代码
#include <thread>
#include <iostream>
#include <chrono>
#include <sstream>
#include <iomanip>
#include <mutex>
using namespace std;

std::mutex mtx;
int x = 0;

void addX(int i)
{
	unique_lock<std::mutex> lockGuard(mtx,defer_lock);
	mtx.lock();
	for (int j = 0; j < i; j++)
	{
		++x;
	}
}

int main()
{
	thread t1(addX, 10);
	thread t2(addX, 20);
	t1.join();
	t2.join();
	std::cout << x << "\n";
	return 0;
}

此时unique_lock仍会帮助我们进行解锁。

那如果是使用它的try_to_lock标识符呢,我怎么知道它是否锁上了呢,unique_lock本身重载了bool:

它和unique_lock中的owns_lock功能一致,我们来看下面这样一个例子:

cpp 复制代码
#include <thread>
#include <iostream>
#include <chrono>
#include <sstream>
#include <iomanip>
#include <mutex>
using namespace std;

std::mutex mtx;
int x = 0;

void addX(int i,const std::string& name)
{
	this_thread::sleep_for(std::chrono::seconds(1));
	unique_lock<std::mutex> unLock(mtx,try_to_lock);
	if (unLock)
	{
		for (int j = 0; j < i; j++)
		{
			++x;
		}
	}
	else {
		std::cout << "name:" << name << "抢夺锁失败!\n";
	}
}

int main()
{
	thread t1(addX, 10,"线程1");
	thread t2(addX, 20,"线程2");
	t1.join();
	t2.join();
	std::cout << x << "\n";
	return 0;
}

结果是随机的,毕竟sleep_for的睡眠时间大于等于1s。

当然,如果我们使用unique_lock替代我们进行锁的接管了,我们仍可以使用unique_lock对锁进行一系列的操作:

如果不想让此unique_lock继续管理锁的话。也可以使用它的release函数释放其对锁的接管权:

五.lock,try_lock与call_once

lock

lock

试想这样一个场景:

cpp 复制代码
#include <thread>
#include <iostream>
#include <chrono>
#include <sstream>
#include <iomanip>
#include <mutex>
using namespace std;

std::mutex mtx1;
std::mutex mtx2;
int x = 0;

void addX1(int i,const std::string& name)
{
	this_thread::sleep_for(std::chrono::seconds(1));
	mtx1.lock();
	mtx2.lock();
	std::cout << name << "\n";
	mtx1.unlock();
	mtx2.unlock();
}

void addX2(int i, const std::string& name)
{
	this_thread::sleep_for(std::chrono::seconds(1));
	mtx2.lock();
	mtx1.lock();
	std::cout << name << "\n";
	mtx1.unlock();
	mtx2.unlock();
}

int main()
{
	thread t1(addX1, 10,"线程1");
	thread t2(addX2, 20,"线程2");
	t1.join();
	t2.join();
	std::cout << x << "\n";
	return 0;
}

一个线程需要同时拥有两个锁时,像上面的情况就容易引发死锁的问题。这时就可以使用c++11为我们提供的一个函数std::lock解决此问题:

cpp 复制代码
#include <thread>
#include <iostream>
#include <chrono>
#include <sstream>
#include <iomanip>
#include <mutex>
using namespace std;

std::mutex mtx1;
std::mutex mtx2;
int x = 0;

void addX1(int i,const std::string& name)
{
	this_thread::sleep_for(std::chrono::seconds(1));
	std::lock(mtx1, mtx2);
	std::cout << name << "\n";
	mtx1.unlock();
	mtx2.unlock();
}

void addX2(int i, const std::string& name)
{
	this_thread::sleep_for(std::chrono::seconds(1));
	std::lock(mtx1, mtx2);
	std::cout << name << "\n";
	mtx1.unlock();
	mtx2.unlock();
}

int main()
{
	thread t1(addX1, 10,"线程1");
	thread t2(addX2, 20,"线程2");
	t1.join();
	t2.join();
	std::cout << x << "\n";
	return 0;
}

当然如果在两个锁解锁过程中又进行了其他操作而抛出异常导致锁无法被正常解除,更安全的写法应该是这样的:

cpp 复制代码
#include <thread>
#include <iostream>
#include <chrono>
#include <sstream>
#include <iomanip>
#include <mutex>
using namespace std;

std::mutex mtx1;
std::mutex mtx2;
int x = 0;

void addX1(int i,const std::string& name)
{
	this_thread::sleep_for(std::chrono::seconds(1));
	std::unique_lock<std::mutex> unLock1(mtx1, std::defer_lock);
	std::unique_lock<std::mutex> unLock2(mtx2, std::defer_lock);
	std::lock(mtx1, mtx2);
	std::cout << name << "\n";
}

void addX2(int i, const std::string& name)
{
	this_thread::sleep_for(std::chrono::seconds(1));
	std::lock(mtx1, mtx2);
	std::unique_lock<std::mutex> unLock1(mtx1, std::adopt_lock);
	std::unique_lock<std::mutex> unLock2(mtx2, std::adopt_lock);
	std::cout << name << "\n";
}

int main()
{
	thread t1(addX1, 10,"线程1");
	thread t2(addX2, 20,"线程2");
	t1.join();
	t2.join();
	std::cout << x << "\n";
	return 0;
}

std::lock的工作方式是,他会同时对所有锁进行上锁,但凡有一个锁没有加锁成功,他会解锁先前所有的加锁成功的锁。这样就避免了我们上面说的死锁问题。

try_lock

try_lock

try_lock也是⼀个函数模板,尝试对多个锁对象进⾏同时尝试锁定,如果全部锁对象都锁定了,返回-1,如果某⼀个锁对象尝试锁定失败,把已经锁定成功的锁对象解锁,并则返回这个对象的下标,第⼀个参数对象,下标从0开始算

cpp 复制代码
// std::lock example
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::try_lock

std::mutex foo,bar;

void task_a () {
  foo.lock();
  std::cout << "task a\n";
  bar.lock();
  // ...
  foo.unlock();
  bar.unlock();
}

void task_b () {
  int x = try_lock(bar,foo);
  if (x==-1) {
    std::cout << "task b\n";
    // ...
    bar.unlock();
    foo.unlock();
  }
  else {
    std::cout << "[task b failed: mutex " << (x?"foo":"bar") << " locked]\n";
  }
}

int main ()
{
  std::thread th1 (task_a);
  std::thread th2 (task_b);

  th1.join();
  th2.join();

  return 0;
}

call_once

call_once

多线程执⾏时,让第⼀个线程执⾏Fn⼀次,其他线程不再执行Fn。听着可能有些难以理解,但是我们看文档中给出的例子就很好明白了:

cpp 复制代码
// call_once example
#include <iostream>       // std::cout
#include <thread>         // std::thread, std::this_thread::sleep_for
#include <chrono>         // std::chrono::milliseconds
#include <mutex>          // std::call_once, std::once_flag

int winner;
void set_winner (int x) { winner = x; }
std::once_flag winner_flag;

void wait_1000ms (int id) {
  // count to 1000, waiting 1ms between increments:
  for (int i=0; i<1000; ++i)
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
  // claim to be the winner (only the first such call is executed):
  std::call_once (winner_flag,set_winner,id);
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(wait_1000ms,i+1);

  std::cout << "waiting for the first among 10 threads to count 1000 ms...\n";

  for (auto& th : threads) th.join();
  std::cout << "winner thread: " << winner << '\n';

  return 0;
}

也就是说,让每个线程都等待1000 milliseconds,醒来之后只有第一个抢到call_once执行权的线程才能执行set_winner这个函数,我们运行下这个程序看下(最终胜者不一定如图所示,每个线程都有可能):

六.atomic

atomic

atomic是⼀个模板的实例化和全特化均定义的原⼦类型,他可以保证对⼀个原⼦对象的操作是线程安全的。atomic对T类型的要求模板可⽤任何满⾜可复制构造 (CopyConstructible) 及可复制赋值 (CopyAssignable) 的可平凡复制 (TriviallyCopyable) 类型 T 实例化,T类型⽤以下⼏个函数判断时,如果⼀个返回false,则⽤于atomic不是原⼦操作。

cpp 复制代码
std::is_trivially_copyable<T>::value
std::is_copy_constructible<T>::value
std::is_move_constructible<T>::value
std::is_copy_assignable<T>::value
std::is_move_assignable<T>::value
std::is_same<T, typename std::remove_cv<T>::type>::value

看下面一个例子:

cpp 复制代码
struct Node {
	int val;
	Node* next;
};

struct Date {
	int day;
	int month;
	int year;
};

template<typename T>
bool check()
{
	return std::is_trivially_copyable<T>::value &&
			std::is_copy_constructible<T>::value &&
		std::is_move_constructible<T>::value &&
		std::is_copy_assignable<T>::value &&
		std::is_move_assignable<T>::value &&
		std::is_same<T, typename std::remove_cv<T>::type>::value;
}

int main() {
	std::cout << check<Node>() << std::endl;
	std::cout << check<Node*>() << std::endl;
	std::cout << check<Date>() << std::endl;
	std::cout << check<Date*>() << std::endl;
	std::cout << check<std::vector<int>>() << std::endl;
	return 0;
}

仅有vector是用于atomic不是原子化操作的。

atomic对于整形和指针⽀持基本加减运算和位运算:

也就是说如果使用atmoic,我们可以直接无锁的对整型或指针类型进行加减操作,我们以整型为例子:

cpp 复制代码
std::atomic_int num1 = 0;
int num2 = 0;

int main()
{
	std::vector<std::thread> threads(2);
	for (int i = 0; i < threads.size(); i++)
	{
		threads[i] = std::thread([&](int k) {
			for (int j = 0; j < k; j++)
			{
				num1++;
				num2++;
			}
		}, 10000);
	}
	for (auto& t : threads)
	{
		t.join();
	}
	std::cout << "原子操作:" << num1 << "\n";
	std::cout << "非原子操作:" << num2 << "\n";
	return 0;
}

load和store可以原⼦的读取和修改atomic封装存储的T对象。

CAS接口

atomic_compare_exchange_weak 与atomic_compare_exchange_strong

注意,atomic的原子化操作主要依赖于硬件的支持。现代处理器提供了原⼦指令来⽀持原⼦操作。例如,在 x86 架构中有CMPXCHG(⽐较并交换)指令。这些原⼦指令能够在⼀个不可分割的操作中完成对内存的读取、⽐较和写⼊操作,简称CAS,Compare And Set,或是 Compare And Swap。另外为了处理多个处理器缓存之间的数据⼀致性问题,硬件采⽤了缓存⼀致性协议,当⼀个atomic操作修改了⼀个变量的值,缓存⼀致性协议会确保其他处理器缓存中的相同变量副本被正确地更新或标记为⽆效。具体可以参考下⾯的代码结合理解⼀下

cpp 复制代码
// gcc⽀持的CAS接⼝
bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval);
type __sync_val_compare_and_swap (type *ptr, type oldval type newval);
// Windows⽀持的CAS接⼝
InterlockedCompareExchange ( __inout LONG volatile *Target,
__in LONG Exchange,
__in LONG Comperand);
// C++11⽀持的CAS接⼝
template <class T>
bool atomic_compare_exchange_weak (atomic<T>* obj, T* expected, T val) noexcept;
template <class T>
bool atomic_compare_exchange_strong (atomic<T>* obj, T* expected, T val) noexcept;
// C++11中atomic类的成员函数
bool compare_exchange_weak (T& expected, T val,
memory_order sync = memory_order_seq_cst) noexcept;
bool compare_exchange_strong (T& expected, T val,
memory_order sync = memory_order_seq_cst) noexcept;

我们以c++11提供的CAS接口写一个样例对其进行了解,比如以上面atomic的那个例子为基础我们使用CAS接口对其进行修改:

cpp 复制代码
std::atomic_int num1 = 0;
int num2 = 0;

int main()
{
	std::vector<std::thread> threads(2);
	for (int i = 0; i < threads.size(); i++)
	{
		threads[i] = std::thread([&](int k) {
			for (int j = 0; j < k; j++)
			{
				int old = num1.load();
				num2++;
				while(!std::atomic_compare_exchange_weak(&num1,&old,old+1)){}
			}
			}, 10000);
	}
	for (auto& t : threads)
	{
		t.join();
	}
	std::cout << "原子操作:" << num1 << "\n";
	std::cout << "非原子操作:" << num2 << "\n";
	return 0;
}

它的原理是:首先num1.load() 是一个原子读取操作,它保证了线程会原子地、不可分割地获取 num1 当前的值,并将其赋值给局部变量(例如 old)。它确保了在读取期间,其他线程的写操作不会干扰该读取过程。接下来执行atomic_compare_exchange_weak时,old会与num的值进行比较,如果因为在此之前执行了别的操作至使其他线程修改了num的值导致了old与num值不相等,修改old值为num的值并返回false,此过程也是原子的。若相等,将num值修改为old+1的值,并返回true,此过程也是原子化操作。

但细心的读者可能发现了,为什么这里又要有一个strong版本的呢,这是因为weak会有一个伪失败的问题而 strong 版本则是对这种现实的封装 。 在许多现代处理器架构(特别是ARM、PowerPC等RISC架构)中,原子比较交换并不是通过单条复杂的指令完成的,而是依赖于"加载链接/存储条件"(LL/SC)这样的一对指令来实现。当CPU执行这种操作时,它会先标记一个缓存行,如果在"读取"和"写入"这两个指令执行的微小时间隙内,发生了上下文切换、中断,或者由于缓存一致性协议(如MESI)导致其他核心"嗅探"或请求了该缓存行,硬件就会为了保证数据安全而强制让当前的存储操作失败。这种情况被称为"伪失败"(Spurious Failure)------即虽然内存中的值仍然等于 old 值,但因为缓存行的状态被干扰了,CAS依然返回 false。
以下是基于google gemini 3pro的回答和我自己进行修正之后给出的一个例子:

假设在一个 64字节 的缓存行(Cache Line)中,紧挨着存放了两个原子变量:

  • 变量 A(由线程 1 操作)
  • 变量 B(由线程 2 操作)

虽然逻辑上它们毫无关系,但在物理内存和缓存中,它们像连体婴儿一样住在同一个"房间"(缓存行)里。
步骤演示:
加载链接 (LL) - 线程 1 线程 1 读取变量 A。此时,CPU 1 会在这一整条缓存行上打个"标记"(Reservation),心里想着:"我要盯着这个房间,等会儿我要改 A,谁也别动这间房。"
干扰发生 (Interference) - 线程 2 就在线程 1 准备修改 A 还没来得及动手时,CPU 2 突然要修改变量 B。 根据 MESI 协议(缓存一致性协议),要修改数据,必须先独占这个缓存行。于是 CPU 2 发出一个信号:"这间房归我了,其他人的备份全部作废!"
协议生效 - 缓存行失效 CPU 1 收到信号,被迫将自己缓存里的这一整行数据(包含 A 和 B)标记为失效(Invalid)。 关键点来了: 虽然 CPU 2 改的是 B,根本没碰 A,但因为它们在同一行,CPU 1 之前打在 A 上的那个"标记"也被无情地抹去了。
存储条件 (SC) - 线程 1 线程 1 终于计算完了,执行 weak_cas 的最后一步------尝试写入 A。 硬件去检查标记,发现:"咦?标记没了?刚才肯定有人动过内存了(虽然其实动的是 B)。" 为了安全起见,硬件直接让这次操作失败,返回 false。
结果: 变量 A 的值根本没变(还是等于 old),但 CAS 报错失败了。这就是典型的由缓存一致性流量导致的伪失败。

不过日常还是使用weak版本更多些 ,因为它既然避免了这种伪失败的问题,底层必定会带来一定程度上的性能损耗。而且在实现高并发的无锁(lock-free)算法时,CAS 操作几乎总是被放在一个 while 循环中。在这种循环中:即使 _weak 偶尔发生伪失败,由于循环的存在,它会立即重试,整体性能影响微乎其微。其次_weak 版本的指令序列在许多现代 CPU 架构上(特别是 ARM/RISC 架构)更简单、更快,因为它避免了为了消除伪失败而必须进行的额外硬件检查或更严格的内存栅栏操作。
使用weak版本也是属于一种工程性考量,不去钻极低概率错误的牛角尖以获取更高的性能。

六种内存重排序模型

可以看到我们的weak后面还带上了一个参数位,这里就需要去讨论下六种重排序模型了:

  1. memory_order_relaxed最宽松的内存顺序,仅保证原⼦操作的原⼦性,不提供任何同步或顺序约束。使⽤场景:适⽤于不需要同步的场景,例如计数器或统计信息。
cpp 复制代码
std::atomic<int> x(0);
x.store(42, std::memory_order_relaxed); // 仅保证原⼦性
  1. memory_order_consume限制较弱的内存顺序,仅保证依赖于当前加载操作的数据的可⻅性。通常⽤于数据依赖的场景。使⽤场景:适⽤于某些特定的依赖链场景,但实际使⽤较少。
cpp 复制代码
std::atomic<int*> ptr(nullptr);
int* p = ptr.load(std::memory_order_consume);
if (p) {
	int value = *p; // 保证 p 指向的数据是可⻅的
}
  1. memory_order_acquire保证当前操作之前的所有读写操作(在当前线程中)不会被重排序到当前操作之后。通常⽤于加载操作。使⽤场景:⽤于实现锁或同步机制中的"获取"操作
cpp 复制代码
std::atomic<bool> flag(false);
int data = 0;
// 线程 1
data = 42;
flag.store(true, std::memory_order_release);
// 线程 2
while (!flag.load(std::memory_order_acquire)) {}
std::cout << data; // 保证看到 data = 42
  1. memory_order_release保证当前操作之后的所有读写操作(在当前线程中)不会被重排序到当前操作之前。通常⽤于存储操作。使⽤场景:⽤于实现锁或同步机制中的"释放"操作。
cpp 复制代码
std::atomic<bool> flag(false);
int data = 0
// 线程 1
data = 42;
flag.store(true, std::memory_order_release); // 保证 data = 42 在 flag = true 之前可⻅
// 线程 2
while (!flag.load(std::memory_order_acquire)) {}
std::cout << data; // 保证看到 data = 42
  1. memory_order_acq_rel结合了 memory_order_acquire 和 memory_order_release 的语义。适⽤于读-修改-写操作(如 fetch_add 或 compare_exchange_strong)。使⽤场景:⽤于需要同时实现"获取"和"释放"语义的操作。
cpp 复制代码
std::atomic<int> x(0);
x.fetch_add(1, std::memory_order_acq_rel); // 保证前后的操作不会被重排序
  1. memory_order_seq_cst最严格的内存顺序,保证所有线程看到的操作顺序是⼀致的(全局顺序⼀致性)。**这也是默认的内存顺序。**使⽤场景:适⽤于需要强⼀致性的场景,但性能开销较⼤。
cpp 复制代码
std::atomic<int> x(0);
x.store(42, std::memory_order_seq_cst); // 全局顺序⼀致性
int value = x.load(std::memory_order_seq_cst);

内存顺序的关系, 宽松到严格:memory_order_relaxed < memory_order_consume <memory_order_acquire < memory_order_release < memory_order_acq_rel <memory_order_seq_cst 。宽松的内存顺序(如 memory_order_relaxed )性能最好,但同步语义最弱。严格的内存顺序(如 memory_order_seq_cst )性能最差,但同步语义最强。

比如我们看如下一个例子:

cpp 复制代码
template<typename T>
struct node
{
	T data;
	node* next;
	node(const T& data) : data(data), next(nullptr) {}
};
namespace lock_free
{
	template<typename T>
	class stack
	{
	public:
		std::atomic<node<T>*> head = nullptr;
	public:
		void push(const T& data)
		{
			node<T>* new_node = new node<T>(data);
			// 将 head 的当前值放到 new_node->next 中
			new_node->next = head.load(std::memory_order_relaxed);
			// 现在令 new_node 为新的 head ,但如果 head 不再是
			// 存储于 new_node->next 的值(某些其他线程必须在刚才插⼊结点)
			// 那么将新的 head 放到 new_node->next 中并再尝试
			while (!head.compare_exchange_weak(new_node->next, new_node,
				std::memory_order_release,
				std::memory_order_relaxed))
				; // 循环体为空
		}
	};
}
namespace lock
{
	template<typename T>
	class stack
	{
	public:
		node<T>* head = nullptr;
		void push(const T& data)
		{
			node<T>* new_node = new node<T>(data);
			new_node->next = head;
			head = new_node;
		}
	};
}

int main()
{
	lock_free::stack<int> st1;
	lock::stack<int> st2;
	std::mutex mtx;
	int n = 1000000;
	auto lock_free_stack = [&st1, n] {
		for (size_t i = 0; i < n; i++)
		{
			st1.push(i);
		}
		};
	auto lock_stack = [&st2, &mtx, n] {
		for (size_t i = 0; i < n; i++)
		{
			std::lock_guard<std::mutex> lock(mtx);
			st2.push(i);
		}
		};
	// 4个线程分别使⽤⽆锁⽅式和有锁⽅式插⼊n个数据到栈中对⽐性能
	size_t begin1 = clock();
	std::vector<std::thread> threads1;
	for (size_t i = 0; i < 4; i++)
	{
		threads1.emplace_back(lock_free_stack);
	}
	for (auto& th : threads1)
		th.join();
	size_t end1 = clock();
	std::cout << end1 - begin1 << std::endl;
	size_t begin2 = clock();
	std::vector<std::thread> threads2;
	for (size_t i = 0; i < 4; i++)
	{
		threads2.emplace_back(lock_stack);
	}
	for (auto& th : threads2)
		th.join();
	size_t end2 = clock();
	std::cout << end2 - begin2 << std::endl;
	return 0;
}

记得使用release版本运行,debug版本下运行结果不太准确:

可以看到明显快了一倍左右。如果都换用最严格内存重排序模型,这种情况下与原来的版本差别不大。

atomic_flag

atomic_flag 是⼀种原⼦布尔类型。与所有atomic 的特化不同,它保证是免锁的。与atomic 不同,atomic_flag 不提供加载或存储操作。主要提供test_and_set操作将flag原⼦的设置为true并返回之前的值,clear原⼦将flag设置为false。下⾯⼀个样例演⽰了⽤atomic_flag实现⾃旋锁。(注意其必须使用ATOMIC_FLAG_INIT进行初始化)

cpp 复制代码
#include <atomic>
#include <thread>
#include <iostream>
#include <vector>
// 自旋锁(SpinLock)是⼀种忙等待的锁机制,适⽤于锁持有时间⾮常短的场景。
// 在多线程编程中,当⼀个线程尝试获取已被其他线程持有的锁时,自旋锁会让该
// 线程在循环中不断检查锁是否可⽤,而不是进⼊睡眠状态。这种⽅式可以减少上
// 下⽂切换的开销(因为线程进入阻塞状态时需要去保存上下文会有一定的消耗),但在锁竞争激烈或锁持有时间较长的情况下,会导致CPU资源的浪费。
//不过需要注意的是,自旋锁不适用与需要保护临界区上下文跨度比较大的情况。毕竟其他线程在疯狂的进行锁的获取
// 以下是使⽤C++11实现的⼀个简单⾃旋锁⽰例:
class SpinLock
{
private:
	// ATOMIC_FLAG_INIT默认初始化为false
	std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
	void lock()
	{
		// test_and_set将内部值设置为true,并且返回之前的值
		// 第⼀个进来的线程将值原⼦的设置为true,返回false
		// 后⾯进来的线程将原⼦的值设置为true,返回true,所以卡在这⾥空转,
		// 直到第⼀个进去的线程unlock,clear,将值设置为false
		while (flag.test_and_set(std::memory_order_acquire));
	}
	void unlock()
	{
		// clear将值原⼦的设置为false
		flag.clear(std::memory_order_release);
	}
};
// 测试⾃旋锁
void worker(SpinLock& lock, int& sharedValue) {
	lock.lock();
	// 模拟⼀些⼯作
	for (int i = 0; i < 1000000; ++i) {
		++sharedValue;
	}
	lock.unlock();
}

int main() {
	SpinLock lock;
	int sharedValue = 0;
	std::vector<std::thread> threads;
	// 创建多个线程
	for (int i = 0; i < 4; ++i) {
		threads.emplace_back(worker, std::ref(lock), std::ref(sharedValue));
	}
	// 等待所有线程完成
	for (auto& thread : threads) {
		thread.join();
	} 
	std::cout << "Final shared value: " << sharedValue << std::endl;
	return 0;
}

七.condition_variable(条件变量)

condition_variable

条件变量必须与互斥锁进行搭配适用,这是我们之前说多线程的时候就已经清楚的东西。最典型的模型就是生产者消费者模型。

下面是一个使用样例:

cpp 复制代码
// condition_variable::notify_all
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
	std::unique_lock<std::mutex> lck(mtx); while (!ready)
		cv.wait(lck);
	// ...
	std::cout << "thread " << id << '\n';
}
void go() {
	std::unique_lock<std::mutex> lck(mtx);
	ready = true;
	// 通知所有阻塞在条件变量上的线程
	cv.notify_all();
}
int main()
{
	std::thread threads[10];
	// spawn 10 threads:
	for (int i = 0; i < 10; ++i)
		threads[i] = std::thread(print_id, i);
	std::cout << "10 threads ready to race...\n";
	std::this_thread::sleep_for(std::chrono::milliseconds(100));
	go(); // go!
	for (auto& th : threads)
		th.join();
	return 0;
}

使用多线程对奇数偶数进行交错打印

这里我们可以使用下condition_variable 构造函数给的第二种构造方式,最常见的使用方式是给上一个lamada表达式,它的逻辑是如果第二个位置的函数调用之后返回的为false则阻塞,返回true则不阻塞

cpp 复制代码
int main()
{
	std::mutex mtx;
	std::condition_variable con;
	int n = 100;
	bool ok = false;

	std::thread t2([&]()
		{
			std::unique_lock<std::mutex> unLock(mtx);
			for (int i = 1; i <= n; i += 2)
			{
				con.wait(unLock, [&ok]() {return ok; });
				//相当于如下写法上面的一行代码
				//while (!ok)
				//{
				//	con.wait(unLock);
				//}
				std::cout << i << std::endl;
				ok = false;
				con.notify_one();
			}
		});

	std::thread t1([&]()
		{
			std::unique_lock<std::mutex> unLock(mtx);
			for (int i = 0; i <= n; i += 2)
			{
				while (ok)
				{
					con.wait(unLock);
				}
				std::cout << i << std::endl;
				ok = true;
				con.notify_one();
			}
		});

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

情况1 :t1先启动,t2过了⼀会才启动(未启动或者还在排队)

t1启动以后先获取锁,flag是true不会被条件变量阻塞,打印i为0,flag修改为false,i修改为2,再⽤条件变量唤醒其他阻塞线程,但是没有线程等待,循环再继续,再次获取锁,flag刚修改为false了,这时会阻塞在条件变量上,并且解锁,这⾥的逻辑保证了t1不会连续打印。

t2这时开始运⾏,先获取锁,flag被t1修改为false了所以t2不会被条件变量阻塞,t1打印j为1,flag修改为true,j修改为3,再⽤条件变量唤醒其他阻塞线程,t1被唤醒。那么这⾥t1被唤醒以后,也是需要分配时间⽚排队执⾏,这时有2种情况,第⼀种t1没有⽴即执⾏,t2继续执⾏,t2获取锁,但是flag为true,所以阻塞在条件变量并且解锁,过⼀会t1开始执⾏了,flag为true不会被条件变量继续阻塞,打印2,继续上述循环逻辑,就交替打印了。第⼆种t1⽴即执⾏,t1抢占到锁,flag为true不会被条件变量继续阻塞,打印2,i修改为4,flag修改为false,再⽤条件变量唤醒其他阻塞线程,但是没有线程被阻塞,再继续循环逻辑就是t1和t2新⼀轮谁先执⾏或者抢到锁资源的逻辑了,这样也实现了交替打印。
情况2 :t2先启动,t1过了⼀会才启动(未启动或者还在排队)

t2启动以后先获取锁,flag是true会被条件变量阻塞,并且同时解锁。

⼀会后,t1开始运⾏,获取到锁资源,flag是true不会被条件变量阻塞,打印i为0,flag修改为false,i修改为2,再⽤条件变量唤醒阻塞线程t2。跟上⾯类似,t2被唤醒以后也是需要分配时间⽚排队执⾏,这时有2种情况,第⼀种t2没有⽴即执⾏,t1继续执⾏循环,获取锁,但是flag为false,所以阻塞在条件变量并且解锁。过⼀会t2开始执⾏了,flag为false不会被条件变量继续阻塞,打印1,j修改为3,flag修改为true,唤醒阻塞线程t1,这时跟上述逻辑类似,循环往复,就可以实现交替打印了。第⼆种t2⽴即执⾏,t2抢到锁,flag为false不会被条件变量继续阻塞,打印1,j修改为3,flag修改为true,唤醒其他阻塞线程,这会没有线程被其他条件变量阻塞,再继续循环逻辑就是t1和t2新⼀轮谁先执⾏或者抢到锁资源的逻辑了,这样也实现了交替打印。
情况3 :t1和t2⼏乎同时启动

这种情况,本质就是两个线程抢夺先锁资源,t1先抢到就类似情况1,t2先抢到就类似情况2,这⾥就不再细节分析了。

八.future和async

future与async相关接口

需要知道的是,下面绝大多数的接口都是基于c++11的线程库进行封装的。

我们主要说如下的几个接口:

启动策略launch::async(强制异步执行)与launch::deferred(延迟执行)

首先我们先简单的认识下future ,std::future: 模板类,用于接收异步任务(由 std::async、std::packaged_task 或 std::promise 创建)返回的结果,其中 T 是结果的类型。而future除了get之外还有wait,wait_for及wait_until,这些方法都是在等待future这个未来体中的值变为有效,至于各有什么区别,读者可以自行验证。我们来说下wait_for与wait_until的返回值:std::future_status
std::future_status 是一个枚举类型,用于检查异步任务的当前状态,而无需阻塞线程。这通常通过 future.wait_for() 或 future.wait_until() 接口使用。

  • std::future_status::ready: 异步任务已完成,结果或异常已准备好。
  • std::future_status::timeout: 等待超时,任务尚未完成。
  • std::future_status::deferred: 任务被延迟执行(使用了 std::launch::deferred 策略)。
    对于future中的valid我们可以看下面这样一个例子:
cpp 复制代码
int main()
{
    std::promise<void> p;
    std::future<void> f = p.get_future();
 
    std::cout << std::boolalpha;
 
    std::cout << f.valid() << '\n';
    p.set_value();
    std::cout << f.valid() << '\n';
    f.get();
    std::cout << f.valid() << '\n';
}
cpp 复制代码
输出:
true
true
false

这点还不好理解,总的来说是检验future是否具有一个共享状态,共享状态的简单解释可见promise。
future.get(): 阻塞当前线程,直到异步任务完成并返回结果。注意:get() 只能调用一次,否则会抛异常,多次调用是一个未定义行为

async 则是会启动一个新线程去执行我们传入的函数。至于什么时候运行此函数,由我们的启动策略决定。

需要注意的是async返回的future未来体必须显示接收,否则会造成阻塞:

cpp 复制代码
std::async(std::launch::async, compute); // 危险!临时 future 析构会阻塞
auto fut = std::async(std::launch::async, compute); // 安全

那么接下来我们再来说这两个启动策略

先来看如下的一个例子:

cpp 复制代码
#include <iostream>       // std::cout
#include <thread>         // std::thread, std::this_thread::sleep_for
#include <chrono>         // std::chrono::milliseconds
#include <mutex>          // std::call_once, std::once_flag
#include <condition_variable>
#include <atomic>
#include <vector>
#include <future>
using namespace std;

int deferred_task() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    cout << "[Deferred] 任务执行,线程ID: " << this_thread::get_id() << endl;
    return 100;
}

int async_task() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    cout << "[Async] 任务执行,线程ID: " << this_thread::get_id() << endl;
    return 200;
}

void launch_example() {
    cout << "[Main] 主线程ID: " << this_thread::get_id() << endl;

    // 策略 1: std::launch::deferred (延迟执行)
    // 此时不会创建新线程,任务被保存起来
    future<int> future_deferred = async(launch::deferred, deferred_task);

    cout << "[Main] Deferred 任务启动后,主线程继续..." << endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    // 只有调用 get() 时,任务才会在主线程中同步执行
    int result_deferred = future_deferred.get();
    cout << "[Main] Deferred 结果: " << result_deferred << endl;

    // 策略 2: std::launch::async (异步执行)
    // 任务会立即在一个新线程中执行
    future<int> future_async = async(launch::async, async_task);

    cout << "[Main] Async 任务启动后,主线程继续..." << endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    // get() 会等待新线程完成
    int result_async = future_async.get();
    cout << "[Main] Async 结果: " << result_async << endl;
}
// launch_example() 运行结果中,Deferred 任务的线程ID会和主线程ID相同,
// Async 任务的线程ID则会不同。

int main()
{
    launch_example();
    return 0;
}

运行之后,我们会发现主线程休眠完1s之后deferred会再等1s才执行。而async的那个主线程休眠完1s之后立马就打印出了结果。其实,deferred策略是我们在调用future的get时才会执行。而asybc策略则是future未来体被创建时立马执行。

那如果我默认不给任何启动策略呢,比如这样:

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

using namespace std;

// 模拟一个耗时的计算函数
int calculate_sum(int a, int b) {
    // 模拟工作耗时
    this_thread::sleep_for(chrono::seconds(2));
    cout << "[Task] 计算线程ID: " << this_thread::get_id() << endl;
    return a + b;
}

int main() {
    // 1. 使用 std::async 启动异步任务
    // future<int> 是一个承诺,最终会得到 int 类型的结果
    // 默认策略通常是 std::launch::async | std::launch::deferred
    future<int> result_future = async(calculate_sum, 10, 20);

    cout << "[Main] 主线程ID: " << this_thread::get_id() << ",任务已启动,继续执行其他操作..." << endl;
    
    // 可以在这里执行其他操作...
    
    cout << "[Main] 等待任务完成并获取结果..." << endl;
    
    // 2. 使用 future.get() 获取结果
    // get() 会阻塞主线程,直到 calculate_sum 执行完毕
    int sum = result_future.get(); 

    cout << "[Main] 异步任务结果: " << sum << endl;

    return 0;
}

他是一个std::launch::async | std::launch::deferred的启动策略,?到底是用的那种启动策略。他是这样的:
如果系统认为当前有足够的资源,并且创建新线程的开销可以接受,它会选择std::launch::async。
如果系统认为创建新线程开销太大,或者线程池已满,或者基于其他内部优化考量,它可能会选择 std::launch::deferred。

所以这是由系统决定的,我们也可以认为他是一个"未定义行为"。所以我们标题处给了一个强制 的字眼也是因为如此,所以一般使用时建议显式指定启动策略,而不是依赖默认值

promise

promise

如果说我们想要让其他线程能够在执行的时候给我一个值,或者没有给成功的时候给我个异常。那么就可以使用promise结合future进行使用,我们先来认识下什么叫共享状态:

如果说一个std::future是从promise中的get_future方法所获取的,那么std::promise 和它对应的 std::future 之间会通过一个内部的、线程安全的共享状态连接。promise 负责写入数据到这个状态,future 负责从中读取数据。上面的async其实与他返回的future未来体之间就是一个共享状态。我们先来看下它的常用接口:

get_future() 获取关联的 std::future 对象。 这是连接 promise 和 future 的桥梁。只能调用一次。
set_value(T const& value) 成功设置结果。 将 value 写入共享状态。调用后,所有等待该 future 的线程将被唤醒。
set_value(T&& value) 成功设置结果(移动语义)。 效率更高的写入操作。
set_exception(exception_ptr e) 设置一个异常。 将异常指针 e 写入共享状态。future.get() 时将重新抛出该异常。

注意,以上的设置操作均是原子操作

多说无益,我们来看如下的一个例子:

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

using namespace std;

// 线程函数,通过 promise 设置结果
void worker_with_promise(promise<string>&& p) {
    try {
        // 模拟一些计算
        string result = "数据已准备就绪";

        // 传递结果给 future
        p.set_value(result);
    }
    catch (...) {
        // 如果发生异常,传递异常给 future
        p.set_exception(current_exception());
    }
}

int main() {
    // 1. 创建 promise 对象
    promise<string> result_promise;

    // 2. 从 promise 中获取 future 对象
    future<string> result_future = result_promise.get_future();

    // 3. 启动新线程,并将 promise 对象移交给线程
    // 必须使用 std::move(result_promise) 因为 promise 内部有不可复制的资源
    thread t(worker_with_promise, move(result_promise));

    cout << "[Main] 主线程等待结果..." << endl;

    // 4. future.get() 等待并获取 promise 设置的结果
    string data = result_future.get();

    cout << "[Main] 从 future 中获取的结果: " << data << endl;

    t.join();
    return 0;
}

他常见的异常情况有如下两种:

异常情况 触发行为 抛出异常 原因
重复设置结果,对同一个 promise 多次调用 set_value() 或 set_exception()。 std::future_error (Code: promise_already_satisfied) 承诺只能兑现一次
失去共享状态 promise 对象被移动后,或默认构造的空 promise 上,调用 set_value() 或 set_exception()。 std::future_error (Code: no_state) promise 已经不关联任何共享状态

我们来看下面的一个例子

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

using namespace std;

void promise_lifetime_example() {
    cout << "--- promise 生命周期示例 ---" << endl;

    // 1. 创建 promise P1
    promise<int> p1;

    // 2. 将 p1 移动给 p2
    promise<int> p2 = std::move(p1);

    // --- 尝试操作 p1(移动后无效)---
    try {
        cout << "尝试对 p1 (已移动) 调用 set_value..." << endl;
        p1.set_value(10); // 抛出异常!
    }
    catch (const future_error& e) {
        cout << "捕获异常:p1 错误码 " << e.code().message() << " (Code: no_state)" << endl;
    }

    // --- 尝试操作 p2(第一次成功)---
    try {
        cout << "对 p2 调用 set_value (第一次)..." << endl;
        p2.set_value(20); // 成功设置
        cout << "第一次设置成功。" << endl;

        // --- 尝试操作 p2(第二次失败)---
        cout << "对 p2 调用 set_value (第二次)..." << endl;
        p2.set_value(30); // 抛出异常!
    }
    catch (const future_error& e) {
        cout << "捕获异常:p2 错误码 " << e.code().message() << " (Code: promise_already_satisfied)" << endl;
    }
}

int main()
{
    promise_lifetime_example();
	return 0;
}

shared_future

shared_future

我们先来看下文档中的描述:

模板类 std::shared_future 提供了一种访问异步操作结果的机制,类似于 std::future,但允许多个线程等待相同的状态。与 std::future 不同(std::future 只能移动,因此只有一个实例可以引用任何特定的异步结果,因为他只能get一次),std::shared_future 是可复制的,多个 shared future 对象可以引用相同的状态。如果每个线程都通过自己的 shared_future 对象副本来访问相同的状态,那么从多个线程访问相同的状态是安全的。

通俗点来讲就是如果我们future中的值需要被多个线程获取,那么就可以使用shared_future,我们来看一个简单的例子就明白他的用法了:

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

using namespace std;

int power_task(int base) {
    this_thread::sleep_for(chrono::seconds(1));
    return base * base;
}

void consumer(shared_future<int> sh_f, int id) {
    // 多个线程都可以调用 get()
    int result = sh_f.get();
    cout << "[Consumer " << id << "] 获取到结果: " << result << endl;
}

void shared_future_example() {
    // 1. 启动任务并获取一个普通的 future
    future<int> original_future = async(launch::async, power_task, 7);

    // 2. 将 future 转换为 shared_future
    // 注意:转换后 original_future 变为空
    shared_future<int> shared_f = original_future.share();

    // 3. 启动多个线程,将 shared_future 传递给它们(shared_future 可复制)
    vector<thread> consumers;
    for (int i = 1; i <= 3; ++i) {
        consumers.emplace_back(consumer, shared_f, i);
    }

    // 4. 主线程也可以获取结果
    cout << "[Main] 主线程也获取一次结果: " << shared_f.get() << endl;

    for (auto& t : consumers) {
        t.join();
    }
}

int main() {
    shared_future_example();
    return 0;
}

packaged_task

文档中给的说明是该类模板 std::packaged_task 包装任何可调用目标(函数、lambda 表达式、bind 表达式或另一个函数对象),使其可以异步调用。其返回值或抛出的异常会被存储在一个共享状态中,可以通过 std::future 对象访问。与 std::function 一样, std::packaged_task 是一个多态的、支持分配器的容器:存储的可调用目标可能在堆上分配或使用提供的分配器进行分配。

我们先来看一个简单的使用样例:

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

using namespace std;

// 要被包装的函数
int multiply(int a, int b) {
    this_thread::sleep_for(chrono::milliseconds(500));
    return a * b;
}

void packaged_task_example() {
    cout << "--- packaged_task 示例 ---" << endl;

    // 1. 包装一个函数:packaged_task<int(int, int)>
    packaged_task<int(int, int)> task(multiply);
    
    // 2. 获取与其关联的 future
    future<int> result_future = task.get_future();
    
    // 3. 将 packaged_task 移动给新线程执行
    // packaged_task 不可复制,只能移动
    thread t(move(task), 5, 6); 

    cout << "[Main] 主线程等待计算结果..." << endl;
    
    // 4. 获取结果
    int result = result_future.get();
    
    cout << "[Main] 乘法结果: " << result << endl;
    
    t.join();
}

也就是说,我们以前使用线程去执行函数时,是拿不到它的返回值的。packaged_task包装的函数运行完毕之后此函数的返回值会写入到与其保有一个共享状态的future中,我们便可以通过此future来获取线程函数执行的返回值。

我们可以基于packaged_task来写一个简单的线程池:

cpp 复制代码
void workerThread(std::mutex& mtx, std::condition_variable& cond,std::queue<std::packaged_task<int()>>& taskqueue)
{
	while (true)
	{
		std::packaged_task<int()> task;
		{
			std::unique_lock<std::mutex> lock(mtx);
			cond.wait(lock, [&]() {return !taskqueue.empty(); });
			task = std::move(taskqueue.front());
			taskqueue.pop();
		}
		//为空说明没有任务了,退出线程执行
		if (!task.valid()) return;
		task();
		{
			std::lock_guard<std::mutex> lock(mtx);
			std::cout << "任务在线程 " << std::this_thread::get_id()
				<< " 执行完毕" << std::endl;
		}
	}
}

int main()
{
	std::mutex mtx;
	std::condition_variable cond;
	std::queue<std::packaged_task<int()>> taskqueue;
	std::thread workerthread(workerThread, std::ref(mtx), std::ref(cond), std::ref(taskqueue));
	for (int i = 0; i < 5; i++)
	{
		std::packaged_task<int()> task([i]()->int {
			return i * i;
		});
		auto fut = task.get_future();
		{
			std::unique_lock<std::mutex> lock(mtx);
			taskqueue.push(std::move(task));
		}
		cond.notify_one();
		std::cout << "工作线程执行任务完毕,运算值为:" << fut.get();
	}
	// 发送结束信号
	{
		std::lock_guard<std::mutex> lock(mtx);
		taskqueue.emplace();  // 空任务表示结束
	}
	cond.notify_all();
	workerthread.join();
	return 0;
}

也就是说packaged_task本质上是把future与可执行对象进行了一个打包。

综合案例-并行计算素数数量

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

bool isPrime(int n) {
    if (n <= 1) return false;
    if (n == 2) return true;
    if (n % 2 == 0) return false;

    int sqrt_n = sqrt(n);
    for (int i = 3; i <= sqrt_n; i += 2) {
        if (n % i == 0) return false;
    }
    return true;
}

int countPrimes(int start, int end) {
    int count = 0;
    for (int i = start; i <= end; ++i) {
        if (isPrime(i)) ++count;
    }
    return count;
}

int main() {
    const int MAX = 1000000;
    const int THREADS = 4;

    std::vector<std::future<int>> futures;
    int chunk = MAX / THREADS;
    int start = std::clock();
    // 启动多个异步任务
    for (int i = 0; i < THREADS; ++i) {
        int start = i * chunk + 1;
        int end = (i + 1) * chunk;

        futures.push_back(
            std::async(std::launch::async, countPrimes, start, end)
        );
    }

    // 收集结果
    int total = 0;
    for (auto& fut : futures) {
        total += fut.get();
    }
    int end = std::clock();
    std::cout << "1到" << MAX << "之间的素数数量: " << total << "多线程运行时间:" << end - start << std::endl;

    //单线程
    int start1 = std::clock();
    int ret = countPrimes(1, MAX);
    int end1 = std::clock();
    std::cout << "1到" << MAX << "之间的素数数量: " << total << "单线程运行时间:" << end1 - start1 << std::endl;
    return 0;
}

release模式下的运行结果如下:

相关推荐
小灰灰搞电子2 小时前
Rust可以取代C++么?
开发语言·c++·rust
cat三三2 小时前
java之异常
java·开发语言
奇树谦2 小时前
【Qt实战】实现图片缩放、平移与像素级查看功能
开发语言·qt
我命由我123452 小时前
Python Flask 开发问题:ImportError: cannot import name ‘Markup‘ from ‘flask‘
开发语言·后端·python·学习·flask·学习方法·python3.11
wjs20242 小时前
Go 语言指针
开发语言
wuguan_2 小时前
C#:多态函数重载、态符号重载、抽象、虚方法
开发语言·c#
小信啊啊2 小时前
Go语言数组与切片的区别
开发语言·后端·golang
计算机学姐2 小时前
基于php的摄影网站系统
开发语言·vue.js·后端·mysql·php·phpstorm