C++并发编程实战
本文总结梳理了C++并发编程的相关知识,并提供大量的使用案例,在开发过程中可作为参考手册使用。测试用例均在MSVC上进行测试。
线程管理
基本使用
c++中启动一个线程非常简单,创建一个 std::thread 对象并传入一个可执行对象即可(普通函数、成员函数、lambda表达式、仿函数),如果有参数,也一并传入即可。
在C++中,仿函数(functor)是一种特殊的对象,它的行为类似于函数。这是通过在类中重载operator()运算符来实现的。仿函数的主要优点是它们可以像普通函数一样被调用,同时还可以保持状态信息,这是普通函数无法做到的。
创建一个线程,调用普通函数
void commonFunc(int x) {
std::cout << x << std::endl;
}
TEST(Test_basic, test_thread_create) {
std::thread t(commonFunc, 1);
t.join(); //等待线程执行结束
}
创建一个线程,调用仿函数
class MyFunctor {
public:
void operator() (int x) {
i_ += x;
std::cout << "i=" << i_ << std::endl;
}
private:
int i_ = 0;
};
TEST(Test_basic, test_thread_create_use_functor) {
MyFunctor functor;
functor(1); //i=1
std::thread t(functor, 2);
t.join(); // i=3
}
线程等待
在创建一个 std::thread 对象后,操作系统分配线程相应的栈空间, “拷贝”对应的参数,并开始执行(处于就绪状态,如果cpu分配了时间片,就进入运行状态,从操作系统层面看,线程可能的状态如下图所示)。

当我们启动一个线程后,就要确定是要等待其运行结束(join),还是与主线程分离(detach),否则线程对象在被析构时就会执行 terminate结束程序,所以为了防止主线程退出或者作用域结束导致子线程被析构,进而导致程序结束,必须及时进行join或detach。
std::thread 的析构函数:
~thread() noexcept {
if (joinable()) {
_STD terminate(); // per N4950 [thread.thread.destr]/1
}
}
线程对象在生命周期结束前 必须进行join或detach 且不能抛出异常 否则会导致程序终止
TEST(Test_basic, test_basic_use) {
// 这个变量t在生命周期结束前必须进行join或者detach 且t不能抛出异常 否则都会导致应用结束
std::thread t([&]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "t is running" << std::endl;
//出现任何异常都会导致程序终止
throw std::exception("hi");
});
//这里如果有代码 要进行try-catch 避免抛出异常导致未join
std::cout << "t started before join" << std::endl;
t.join();
std::cout << "t started after join" << std::endl;
}
线程detach
detach之后的线程以分离的方式在后台独自运行,可以理解为java中的守护线程,main线程结束,守护线程会立即结束。下面这个例子在线程 t 未打印完成,主线程就会结束,同时程序也会结束,不会等待 t 执行完成
TEST(Test_basic, test_detach) {
std::thread t([&]() {
std::cout << "t is running" << std::endl;
for (int i = 0; i < 100; ++i) {
std::cout << i << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
});
//detach之后的线程是守护线程,main线程结束,守护线程会立即结束
t.detach();
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "main is finished" << std::endl;
}
注意:线程只能被join或者detach一次,可以通过
joinable()判断,为true才可以进行join或者detach,否则会抛出异常。在join和detach的源码中,会先判断是否
joinable(),否则直接抛出异常:void join() { if (!joinable()) { _Throw_Cpp_error(_INVALID_ARGUMENT); } if (_Thr._Id == _Thrd_id()) { _Throw_Cpp_error(_RESOURCE_DEADLOCK_WOULD_OCCUR); } if (_Thrd_join(_Thr, nullptr) != _Thrd_result::_Success) { _Throw_Cpp_error(_NO_SUCH_PROCESS); } _Thr = {}; } void detach() { if (!joinable()) { _Throw_Cpp_error(_INVALID_ARGUMENT); } if (_Thrd_detach(_Thr) != _Thrd_result::_Success) { _Throw_Cpp_error(_INVALID_ARGUMENT); } _Thr = {}; }
线程传参
std::thread 传参机制:在主线程调用构造函数时,所有参数会被先拷贝到新线程的栈空间,原始参数类型、实参类型决定参数传递方式,参数不匹配时可能发生隐式转换,否则编译失败
- 引用、指针等参数传参时实参的生命周期问题:通过传递给std::thread对象参数,来传递给要执行的函数,如果存在参数类型转换,注意在detach之后,可能参数转换还未完成,导致未定义行为。以下程序大概率会崩溃:
TEST(Test_basic, test_pass_parameter) {
{
std::string s;
for (int i = 0; i < 1000000; ++i) {
s.append("h");
}
const char *ptr = s.c_str();
//std::thread对象创建之后线程就立即执行了 std::thread的参数也会进行拷贝 进入线程的上下文环境后才进行ptr到string的转换
std::thread t([](const std::string &s) {
std::cout << "s size is: " << s.size() << std::endl;
}, ptr); //ptr隐式转换为string 避免ptr为悬空指针
// std::thread t([](const std::string &s) {
// std::cout << "s size is: " << s.size() << std::endl;
// }, std::string(ptr)); //传参时显式转换为string作为参数
t.detach();
std::cout << "t is detached" << std::endl;
} //作用域结束 ptr释放 线程t隐式转换ptr为string异常
std::this_thread::sleep_for(std::chrono::seconds(2));
}
注意:慎用线程参数的隐式类型转换,对于小对象,可以采用拷贝,对于大对象,可以采用智能指针传递参数
- 引用传参,使用
std::ref(),std::cref(),默认是拷贝(也不一定)
/**
address0: 000000D0F91EE430
address1: 000002AA9C751390
address2: 000002AA9C751210
address3: 000000D0F91EE430
address4: 000000D0F91EE430
address0: 000000B1A8FBE5C0
address1: 000001CA1F5BDC90
address2: 000001CA1F5BDC90
address3: 000000B1A8FBE5C0
address4: 000000B1A8FBE5C0
*/
TEST(Test_basic, test_pass_parameter_for_ref) {
std::string s1 = "hello";
std::cout << "address0: " << &s1 << std::endl;
{
//函数参数会默认拷贝 const引用和非const引用都会拷贝
std::thread t([](const std::string &s) {
std::cout << "address1: " << &s << std::endl;
}, s1);
t.join();
}
{
std::thread t([](std::string &s) { //书上说这里会编译错误 msvc没有编译错误 可能跟编译器有关
std::cout << "address2: " << &s << std::endl;
}, s1); //有时address2与address1相同 有时不同 可能是msvc编译器的优化
t.join();
}
{
//使用std::ref传递引用
std::thread t([](std::string &s) {
std::cout << "address3: " << &s << std::endl;
}, std::ref(s1));
t.join();
}
{
std::string &s2 = s1;
//使用std::cref传递const引用 显式指明不希望修改引用
std::thread t([](const std::string &s) {
std::cout << "address4: " << &s << std::endl;
// auto &scopy = const_cast<std::string &>(s); //强制修改 不管std::ref和std::cref
// scopy.append("abc");
}, std::cref(s2));
t.join();
// std::cout << "final: " << s1 << std::endl; //helloabc
}
std::this_thread::sleep_for(std::chrono::seconds(2));
}
如果参数是独占的,比如 std::unique_ptr ,则传递参数时通过 std::move 转移所有权即可
- 对象调用与拷贝对象调用
class C1 {
public:
void func1(int x) {
i_ += x;
std::cout << "f1: " << i_ << std::endl;
}
private:
int i_ = 0;
};
void func2() {
std::cout << "f2" << std::endl;
}
TEST(Test_basic, test_pass_parameter_for_method) {
auto obj = C1();
//实际方法调用 对象是方法的第一个参数
// obj.func1(); --> C1::func1(ob1)
//普通函数调用 编译器会自动转换func2函数的地址
// std::thread t(func2);
std::thread t(&func2); //传func2 和&func2都可以
t.join();
std::thread t2(&C1::func1, obj, 1); //obj的对象拷贝调用 输出: f1: 1
t2.join();
std::thread t3(&C1::func1, &obj, 1); //obj调用 输出: f1: 1
t3.join();
}
线程归属权
线程归属权语义与std::unique_ptr一样,只可转移,不可复制。
void func3() {
std::cout << "func3" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
void func4() {
std::cout << "func4" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
TEST(Test_basic, test_thread_move) {
std::thread t1(func3);
//把t1转移给t2 转移后t1无效
std::thread t2 = std::move(t1);
//t1又可以重新绑定线程了
t1 = std::thread(func4);
t1.join();
t2.join();
//t1执行完后可以继续绑定线程
t1 = std::thread(func3);
t1.join();
std::cout << "next test" << std::endl;
std::flush(std::cout);
//如果一个线程还没有执行完就绑定新线程 会导致程序终止 (更准确的说是没有执行join或detach)
std::thread t3(func3);
std::thread t4(func4);
t3 = std::move(t4); //t3正在执行 就将t4转移给t3会导致t3析构 导致程序终止
t3.join(); //t4已经被move了 没有办法join了 也不符合std::thread必须join或者detach的要求
std::this_thread::sleep_for(std::chrono::minutes(5)); //程序会立刻退出 说明没有执行到这里
}
//和std::unique_ptr一样 可以函数返回thread
std::thread return_thread() {
std::thread t(func3);
return t; //c++11 返回局部变量 先使用移动构造 没有则使用拷贝构造
}
TEST(Test_basic, test_thread_move2){
std::thread t = return_thread();
t.join();
}
在 std::thread 的拷贝构造函数里,会先判断当前函数是否是 joinable() ,如果没有调用过就为其赋值一个新的thread,也会调用 terminate,移动赋值源码:
thread& operator=(thread&& _Other) noexcept {
if (joinable()) {
_STD terminate(); // per N4950 [thread.thread.assign]/1
}
_Thr = _STD exchange(_Other._Thr, {});
return *this;
}
线程间共享数据
由于对数据的操作不是原子的,当多个线程同时操作一份共享数据时(只读除外),可能会出现未定义行为,因此,对共享数据的读写操作,需要进行保护,一般使用互斥量(锁)保护共享数据。
基本使用
对共享数据操作前lock,操作完成后unlock,不过一般不直接使用 std::mutex
TEST(Test_basic, test_mutex) {
//使用互斥量 (不建议的做法 需要手动管理锁)
{
std::mutex m; //不可递归
// std::recursive_mutex m1; //递归互斥量
m.lock();
//access shared data
m.unlock();
}
//使用RAII管理互斥量
{
std::mutex m;
std::lock_guard lock(m); //构造即获得锁
//access shared data
} //作用域结束 自动释放锁
//使用RAII管理互斥量 unique_lock更灵活 (延迟加锁、接收已加锁的互斥量、提前解锁)
{
std::mutex m;
std::unique_lock lock(m); //构造即获得锁
//延迟加锁
// std::unique_lock lock(m, std::defer_lock);
// lock.lock();
//接收已加锁的互斥量
// m.lock();
// std::unique_lock lock(m, std::adopt_lock);
//提前解锁 std::lock_guard没有unlock
// lock.unlock();
//access shared data
} //作用域结束 自动释放锁
//使用RAII管理互斥量 c++17增强版的lock_guard 可直接替换lock_guard 可同时管理多个互斥量
//可结合std::defer_lock延迟加锁/std::adopt_lock接管已加锁的互斥量(c++20)
{
std::mutex m;
std::mutex m2;
std::scoped_lock lock(m, m2); //构造即获得锁
//access shared data
} //作用域结束 自动释放锁
}
std::lock_guard
lock_guard 在作用域结束时自动调用其析构函数解锁,这么做的一个好处是简化了一些特殊情况从函数中返回的写法,比如异常或者条件不满足时,函数内部直接return,锁也会自动释放。
TEST(Test_basic, test_lock_guard) {
//使用RAII管理互斥量 lock_guard对mutex进行了封装 析构函数中自动释放锁
std::mutex m;
{
std::lock_guard lock(m); //构造即获得锁
//access shared data
} //作用域结束 自动释放锁
{
m.lock(); //已经获得锁了
std::lock_guard lock(m, std::adopt_lock); //领养锁 使用lock_guard自动释放锁
//access shared data
} //作用域结束 自动释放锁
}
std::unique_lock
unique_lock 和 lock_guard 基本用法相同,构造时默认加锁,析构时默认解锁,但unique_lock 更灵活,可以手动解锁,方便控制锁的范围,也可以延迟加锁、接收已加锁的互斥量等。
TEST(Test_basic, test_unique_lock) {
//使用RAII管理互斥量 unique_lock更灵活 (延迟加锁、接收已加锁的互斥量、提前解锁等)
std::mutex m;
{
std::unique_lock lock(m); //构造即获得锁
//access shared data
EXPECT_TRUE(lock.owns_lock());
} //作用域结束 自动释放锁
{
std::unique_lock lock(m);
//access shared data
//提前解锁 std::lock_guard没有unlock
lock.unlock();
// m.unlock(); //错误用法 锁交给unique_lock后就不要通过mutex操作锁了
EXPECT_FALSE(lock.owns_lock());
}
{
//延迟加锁
std::unique_lock lock(m, std::defer_lock);
lock.lock();
}
{
//接收已加锁的互斥量
m.lock();
std::unique_lock lock(m, std::adopt_lock);
EXPECT_TRUE(lock.owns_lock());
}
}
递归锁
std::mutex 是独占的,且只能获取一次,如果需要多次获取锁,可以使用 std::recursive_mutex
TEST(Test_basic, test_recursive_mutex) {
class Service {
public:
void func1() {
std::unique_lock lock(mutex_);
std::cout << "do some work in func1" << std::endl;
func2(); //重复获取锁 不能使用std::mutex
}
void func2() {
std::unique_lock lock(mutex_);
std::cout << "do some work in func2" << std::endl;
}
private:
std::recursive_mutex mutex_;
};
Service service;
service.func1();
}
共享锁(读写锁)
读操作并不是互斥的,同一时间可以有多个线程同时读,但是写和读是互斥的,写与写是互斥的,简而言之,写操作需要独占锁。而读操作需要共享锁。对于读多写少的场景,可以使用读写锁提高并发性能。
TEST(Test_basic, test_shared_mutex) {
class Service {
public:
//多线程可以同时读取不阻塞
void read() {
std::shared_lock lock(mutex_);
std::cout << "only read data" << std::endl;
}
//如果有读线程 或者写线程已获得锁 则阻塞
void write() {
std::unique_lock lock(mutex_);
std::cout << "modify data" << std::endl;
}
private:
std::shared_mutex mutex_;
};
Service service;
service.read();
service.write();
}
同时加锁避免死锁
当我们无法避免在一个函数内部使用两个互斥量,并且都要解锁的情况,那我们可以采取同时加锁的方式,避免可能的死锁。
TEST(Test_basic, test_multi_mutex) {
std::mutex m1;
std::mutex m2;
//一次获得多个锁以避免死锁
{
//同时获取两个锁
std::lock(m1, m2);
std::lock_guard lock1(m1, std::adopt_lock); //所属权转移给lock_guard 或者unique_lock
std::lock_guard lock2(m2, std::adopt_lock);
//如果不用RAII 需要进行手动释放锁 (不建议)
// m1.unlock();
// m2.unlock();
}
{
//也可以先构造unique_lock 再获取锁 与上面等价
std::unique_lock lock1(m1, std::defer_lock);
std::unique_lock lock2(m2, std::defer_lock);
std::lock(lock1, lock2);
}
{
//c++17中的写法
std::scoped_lock lock3(m1, m2); //自动加锁 并进行RAII管理
}
}
线程安全的初始化
- 使用
std::call_once
C++11 提出了call_once函数,我们可以配合一个局部的静态变量once_flag实现线程安全的初始化。 多线程调用call_once函数时,会判断once_flag是否被初始化,如没被初始化则进入初始化流程,调用我们提供的初始化函数。 但是同一时刻只有一个线程能进入这个初始化函数。
/**
* 多线程初始化并获取一个共享资源 类似单例模式
*/
class Singleton {};
std::shared_ptr<Singleton> resource;
std::once_flag once_flag;
//初始化共享资源
void init() {
//init resource
resource = std::make_shared<Singleton>();
std::cout << "do init" << std::endl;
}
std::shared_ptr<Singleton> getSingleton() {
//多线程调用 只会调用init 一次
std::call_once(once_flag, init);
return resource;
}
TEST(Test_basic, init_once) {
for (int i = 0; i < 10; ++i) {
std::thread t([] {
auto singleton = getSingleton();
}
);
t.detach();
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::shared_ptr<Singleton> init2() {
auto resource = std::make_shared<Singleton>();
std::cout << "do init" << std::endl;
return resource;
}
- c++11,使用static初始化保证只初始化一次
std::shared_ptr<Singleton> init2() {
auto resource = std::make_shared<Singleton>();
std::cout << "do init" << std::endl;
return resource;
}
/**
* c++11 static的变量 多线程只会初始化一次 由编译器保证
*/
std::shared_ptr<Singleton> getSingleton2() {
static std::shared_ptr<Singleton> resource = init2();
return resource;
}
TEST(Test_basic, init_by_static) {
for (int i = 0; i < 10; ++i) {
std::thread t([] {
auto singleton = getSingleton2();
}
);
t.detach();
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
归属权
不可移动,也不可复制
- std::mutex 及其他所有的mutex
- std::lock_guard
- std::once_flag
std::unique_lock 是可移动,不可复制的,可以通过其间接实现锁的传递(与std::thread、std::unique_ptr语义相同)
使用建议
锁的颗粒度
- 不能过大,过大会增加其他线程无畏的等待时间,降低性能
- 不能过小,必须保证保护到所有的共享数据,即保证读写操作的原子性,建议使用
std::unique_lock - 读多写少场景使用读写锁
防止死锁
- 避免嵌套锁,已经持有一个锁,就不要试图获取第二个锁
- 如果多锁是必要的,则按照固定顺序获取锁,或同时加锁
线程间同步/通信
假如要实现一个这样的功能,有三个线程,分别交替打印1/2/3,如果只通过互斥实现,需要频繁判断标志位,效率比较低,且线程之间交互必定不及时,所以就需要用到线程间同步的机制,当不需要线程执行时就阻塞等待,需要其执行时就唤醒它,线程之间互相等待通知的媒介就是条件变量。
条件变量
std::condition_variable 是 C++11 中引入的一个同步原语,用于在多线程程序中进行线程间的同步。它允许一个或多个线程在某个条件为真之前挂起(等待),直到另一个线程修改了条件并通知 std::condition_variable。
std::condition_variable 需要与 std::unique_lock 一起使用,以便在等待条件时释放互斥锁,并在条件满足后重新获取互斥锁。它提供了 wait、notify_one 和 notify_all 方法来实现线程间的等待和通知机制。
std::condition_variable_any,可以与其他任意实现了 lock() 、unlock() 的锁使用
线程间通信的例子,三个线程轮流打印,生产者消费者模型的例子参考后的线程池实现。
/**
* 通过条件变量进行线程间通信
* wait必须在持有锁后执行 否则会导致未定义行为
* notify时无需持有锁 (在保证逻辑正确的前提下 建议释放锁后notify 避免被唤醒线程与当前线程进行锁竞争)
*/
TEST(Test_basic, test_syncrhonize) {
//线程交替打印 t1先打印 打印完通知线程t2 t2打印完通知线程t3 t3打印完通知线程t1
std::mutex mutex;
std::condition_variable condition1; //只能用于同一个lock
std::condition_variable condition2;
std::condition_variable condition3; //这里用了三个condition实现两两之间互相等待唤醒 也可以用一个 但会有很多的伪唤醒
int flag = 1;
std::thread t1([&] {
for (int i = 0; i < 10; ++i) {
//获取锁后打印
std::unique_lock lock(mutex);
//等待其他线程打印
while (flag != 1) {
condition1.wait(lock);
}
std::cout << "t1" << std::endl;
flag = 2;
lock.unlock();
//打印完通知其他线程
condition2.notify_all();
}
});
std::thread t2([&] {
for (int i = 0; i < 10; ++i) {
//获取锁后打印
std::unique_lock lock(mutex);
//等待其他线程打印
while (flag != 2) {
condition2.wait(lock);
}
std::cout << "t2" << std::endl;
flag = 3;
lock.unlock();
//打印完通知其他线程
condition3.notify_all();
}
});
std::thread t3([&] {
for (int i = 0; i < 10; ++i) {
//获取锁后打印
std::unique_lock lock(mutex);
//等待其他线程打印
condition3.wait(lock, [&] { return flag == 3; });
std::cout << "t3" << std::endl;
flag = 1;
lock.unlock();
//打印完通知其他线程
condition1.notify_one();
}
});
t1.join();
t2.join();
t3.join();
}
注意:
- wait必须在持有锁后执行 否则会导致未定义行为
- notify时无需持有锁 (在保证逻辑正确的前提下 建议释放锁后notify 避免被唤醒线程与当前线程进行锁竞争)
- 在调用 wait 方法时,应该使用 while 循环来检查条件,以防止假唤醒(spurious wakeups)。
java中的wait和notify(signal)都必须在获取到锁之后才能调用,且被唤醒线程被notify之后不会立即执行(notify操作只是将线程放入同步队列中),会等当前线程释放锁之后才会真正开始执行(开始竞争锁),c++中notify与java相比,相对更底层些
- wait时未获取锁的例子
/**
* wait时未获取锁会导致未定义行为
* 测试结果:该线程永远不会被唤醒 且其他线程无法再获取到锁
*/
TEST(Test_basic, test_wait_without_lock) {
std::mutex mutex;
std::condition_variable condition;
std::thread t1([&] {
std::cout << "t1 start" << std::endl;
std::unique_lock lock(mutex, std::defer_lock);
// lock.lock();
// std::cout << "t1 got lock" << std::endl;
condition.wait(lock);
std::cout << "t1 finished" << std::boolalpha << lock.owns_lock() << std::endl;
});
t1.detach();
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
//无法获取锁
// std::cout << "main try lock" << std::endl;
// mutex.lock();
// mutex.unlock();
//也无法唤醒wait的线程
condition.notify_all();
std::cout << "notify finished" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
std::async
std::thread 是一个低级别的线程管理工具,直接与操作系统线程交互。它适合需要精确控制线程生命周期的场景,例如手动管理线程的启动、同步和销毁。使用 std::thread 时,线程的返回值无法直接获取,需要通过共享变量或其他同步机制来传递结果。
std::async 是一个更高级的异步任务接口(它是一个异步执行函数的模板函数),它可以根据策略选择是否创建新线程或延迟执行任务,返回一个future对象。
std::future 代表一个异步操作的结果,可以通过future获取任务执行结果或等待其执行完成,使用future关联的任务如果抛异常,会在get()时重新rethrow,这一点相比于 std::thread 要易用很多。
以下是一个使用 std::async 的示例:
TEST(Test_basic, test_future) {
//使用future获取线程执行结果
std::future<int> future = std::async(std::launch::async, [] {
std::cout << "async thread: " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
for (int i = 0; i < 10; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << i << std::endl;
}
//如果抛出异常 会存储在future中 get时重新抛出
// throw std::exception("some exception");
return 1;
});
//do something else
std::cout << "main thread: " << std::this_thread::get_id() << std::endl;
//从future获取异步结果 如果还没有执行完 则阻塞
try {
int result = future.get(); //如果不调用get future析构时仍会阻塞 直到关联的任务执行完成
std::cout << "async thread result: " << result << std::endl;
} catch (const std::exception& e) {
std::cout << "exception: " << e.what() << std::endl;
} catch (...) {
std::cout << "unknown exception" << std::endl;
}
}
值得注意的是,
std::future析构函数会阻塞,直到线程结束。通常我们认为std::future::get()和std::future::wait()才会阻塞,析构函数同样也会,这一点需要特别小心。下面的例子说明了使用
std::thread进行detach,与使用std::async的区别,前者随着主线程结束会立即退出,后者则会等待任务完成后才会退出。TEST(Test_basic, test_future_and_thread) { //thread detach之后 线程会与主线程分离,独立运行。主线程无法再通过 join() 等待它,且线程的资源由运行时自动回收。 //分离后的线程可能随主线程结束而终止(若主线程退出且线程未完成) { std::thread t([] { for (int i = 0; i < 10; ++i) { std::cout << "common thread: " << std::this_thread::get_id() << " " << i << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } ); t.detach(); } //策略std::launch::async启动线程关联的future 其在析构时隐式等待线程结束 类似于隐式join // { // auto future = std::async(std::launch::async, [] { // for (int i = 0; i < 10; ++i) { // std::cout << "async thread: " << std::this_thread::get_id() << " " << i << std::endl; // std::this_thread::sleep_for(std::chrono::milliseconds(200)); // } // }); // } std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "main thread finished: " << std::this_thread::get_id() << std::endl; }
std::async的启动策略
std::async函数可以接受几个不同的启动策略,这些策略在std::launch枚举中定义。除了std::launch::async之外,还有以下启动策略:
std::launch::deferred:这种策略意味着任务将在调用std::future::get()或std::future::wait()函数时执行,不创建新线程,在当前线程里执行。换句话说,任务将在需要结果时才执行。std::launch::async | std::launch::deferred:这种策略是上面两个策略的组合。任务可以在一个单独的线程上异步执行,也可以延迟执行,具体取决于实现。
默认情况下,std::async使用std::launch::async | std::launch::deferred策略。这意味着任务可能异步执行,也可能延迟执行,具体取决于实现。需要注意的是,不同的编译器和操作系统可能会有不同的默认行为。不建议使用,建议明确具体使用哪种策略
TEST(Test_basic, test_future_launch_deferred) {
//deferred参数 不会立即执行 在调用wait或者get时执行 且不创建新线程
std::future<void> future = std::async(std::launch::deferred, [] {
std::cout << "async thread: " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
});
std::cout << "main thread: " << std::this_thread::get_id() << std::endl; //same with async thread
//wait可以多次调用 但是get只能调用一次
future.wait();
future.wait();
// future.get(); //由于调用没有返回值 future.get()也没有返回值
}
std::future的wait和get
std::future::get() 是一个阻塞调用,用于获取 std::future 对象表示的值或异常。如果异步任务还没有完成,get() 会阻塞当前线程,直到任务完成。如果任务已经完成,get() 会立即返回任务的结果。重要的是,get() 只能调用一次,因为它会移动或消耗掉 std::future 对象的状态。一旦 get() 被调用,std::future 对象就不能再被用来获取结果。
std::future::wait() 也是一个阻塞调用,但它与 get() 的主要区别在于 wait() 不会返回任务的结果。它只是等待异步任务完成。如果任务已经完成,wait() 会立即返回。如果任务还没有完成,wait() 会阻塞当前线程,直到任务完成。与 get() 不同,wait() 可以被多次调用,它不会消耗掉 std::future 对象的状态。
总结一下,这两个方法的主要区别在于:
std::future::get()用于获取并返回任务的结果,而std::future::wait()只是等待任务完成。get()只能调用一次,而wait()可以被多次调用。- 如果任务还没有完成,
get()和wait()都会阻塞当前线程,但get()会一直阻塞直到任务完成并返回结果,而wait()只是在等待任务完成。
判断std::future是否ready
future还有 wait_for 、wait_util 操作,用于限时等待,这部分后续介绍,可以通过将 wait_for 的等待时间设置为0,检查其的返回值查看当前future的状态是否ready
/**
* 利用wait_for判断结果是否可用
*/
TEST(Test_basic, test_future_is_ready) {
std::future<int> future = std::async(std::launch::async, [] {
std::cout << "start" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "end" << std::endl;
return 1;
});
EXPECT_FALSE(future.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready);
//future is not ready. may do something else
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT_TRUE(future.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready);
std::cout << "result: " << future.get() << std::endl;
}
std::packaged_task
std::packaged_task 本身和线程没什么关系,它只是一个关联了 std::future 的仿函数,它可以捕获任务的返回值或异常,并将其存储在std::future对象中,以便以后使用。
以下是使用std::packaged_task和std::future的基本步骤:
- 创建一个
std::packaged_task对象,该对象包装了要执行的任务。 - 调用
std::packaged_task对象的get_future()方法,该方法返回一个与任务关联的std::future对象。 - 在另一个线程上调用
std::packaged_task对象的operator(),以执行任务。 - 在需要任务结果的地方,调用与任务关联的
std::future对象的get()方法,以获取任务的返回值或异常。
int myTask(int i) {
std::cout << "myTask " << i << std::endl;
// throw std::exception("myTask exception");
return i;
}
/**
* 使用packaged_task关联future与task
*/
TEST(Test_basic, test_packaged_task) {
std::packaged_task<int(int)> pt(myTask);
std::future<int> future = pt.get_future();
//pt封装了myTask 并关联了一个future pt可以交给其他线程执行 主线程通过future获取task执行的结果
//注意f2的类型是std::future<void> 这个类型是可执行对象pt的返回值类型 pt封装了myTask 无返回值 返回结果在pt关联的future中
std::future<void> f2 = std::async(std::move(pt), 1);
//pt也是一个可调用对象
// pt(1);
try {
std::cout << "result: " << future.get() << std::endl;
} catch (const std::exception& e) {
std::cout << "exception: " << e.what() << std::endl;
} catch (...) {
std::cout << "unknown exception" << std::endl;
}
}
std::promise
std::promise 用于在某一线程中设置某个值或异常,使用其关联的 std::future在另一线程中获取这个值或异常。因此,std::promise 提供了一种线程之间同步的手段。
/**
* 使用promise关联future
* promise用于设置结果 future获取结果
*/
TEST(Test_basic, test_promise) {
std::promise<int> promise;
std::future<int> future = promise.get_future();
//如果是异步任务 需要手动管理thread 然后通过promise设置值
//只能set一次 set多次则抛异常 promise already satisfied
try {
//do something else
promise.set_value(1);
// promise.set_value(1);
} catch (...) {
promise.set_exception(std::current_exception());
}
// promise.set_exception(std::make_exception_ptr(std::runtime_error("some error")));
// promise.set_value_at_thread_exit(-1);
// promise.set_exception_at_thread_exit(std::make_exception_ptr(std::runtime_error("error exit")));
//通过future获取结果 valid()验证当前future是否关联共享状态 get值之后就不关联了
//只能get一次 get多次抛异常 no state
try {
std::cout << "valid: " << std::boolalpha << future.valid() << std::endl;
int result = future.get();
std::cout << "result: " << result << std::endl;
std::cout << "valid: " << std::boolalpha << future.valid() << std::endl;
// future.get();
} catch (const std::exception &e) {
std::cout << "got exception: " << e.what() << std::endl;
} catch (...) {
std::cout << "got unexpected exception" << std::endl;
}
}
future关联的对象提前析构
如果与future关联的packaged_task或者promise在未设置值或未执行时 如果提前析构则future.get()会抛出 broken promise异常
/**
* std::promise或者std::packaged_task在未设置值或未执行时 如果提前析构则future.get会抛出 broken promise异常
*/
TEST(Test_basic, test_promise_error) {
std::promise<int> promise;
auto future = promise.get_future();
//创建一个线程提前析构promise
std::thread t([&promise] {
auto pro = std::move(promise);
});
t.detach();
try {
std::cout << future.get() << std::endl;
} catch (const std::exception &e) {
std::cout << "exception: " << e.what() << std::endl;
} catch (...) {
std::cout << "unknown exception" << std::endl;
}
std::packaged_task<int(int)> pt(myTask);
std::future<int> future2 = pt.get_future();
//创建一个线程提前析构packaged_task
std::thread t2([&pt] {
auto pt2 = std::move(pt);
});
t2.detach();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
try {
std::cout << future2.get() << std::endl;
} catch (const std::exception &e) {
std::cout << "exception2: " << e.what() << std::endl;
} catch (...) {
std::cout << "unknown exception2" << std::endl;
}
}
使用总结
总体来说,std::async 接口最简单,做的事情最多,抽象程度最高;std::packaged_task,抽象程度次之,需要额外的操作但却比较灵活;std::promise 功能最为单一,是三者中抽象程度最低的。
std::async 和 std::packaged_task 底层本质上都依赖 std::promise 提供的线程间同步数据的能力。
std::async 源码中可以看到,它用到了 _Promise
_EXPORT_STD template <class _Fty, class... _ArgTypes>
_NODISCARD_ASYNC future<_Invoke_result_t<decay_t<_Fty>, decay_t<_ArgTypes>...>> async(
launch _Policy, _Fty&& _Fnarg, _ArgTypes&&... _Args) {
// manages a callable object launched with supplied policy
using _Ret = _Invoke_result_t<decay_t<_Fty>, decay_t<_ArgTypes>...>;
using _Ptype = typename _P_arg_type<_Ret>::type;
_Promise<_Ptype> _Pr(
_Get_associated_state<_Ret>(_Policy, _Fake_no_copy_callable_adapter<_Fty, _ArgTypes...>(
_STD forward<_Fty>(_Fnarg), _STD forward<_ArgTypes>(_Args)...)));
return future<_Ret>(_From_raw_state_tag{}, _Pr._Get_state_for_future());
}
std::packaged_task 的源码中也用到了 _Promise
template <class _Ret, class... _ArgTypes>
class packaged_task<_Ret(_ArgTypes...)> {
// class that defines an asynchronous provider that returns the result of a call to a function object
private:
using _Ptype = typename _P_arg_type<_Ret>::type;
using _MyPromiseType = _Promise<_Ptype>;
using _MyStateManagerType = _State_manager<_Ptype>;
using _MyStateType = _Packaged_state<_Ret(_ArgTypes...)>;
而 std::promise 只是对 _Promise 的简单封装:
_EXPORT_STD template <class _Ty>
class promise { // class that defines an asynchronous provider that holds a value
public:
//...
private:
_Promise<_Ty> _MyPromise;
};
归属权
以下类型对象均可移动,不可复制,独占所有权,与std::thread、std::unique_ptr、std::unique_lock语义一致
-
std::future
-
std::packaged_task
-
std::promise
std::shared_future
std::future本身不是线程安全的,且只能get一次,如果需要多个线程同时获取或等某个任务的结果,可以使用std::shared_future,语义上可以理解为与 std::shared_ptr一致。
TEST(Test_basic, test_shared_future) {
//使用future的隐式转换
{
std::promise<int> promise;
std::shared_future<int> shared_future = promise.get_future(); //自动将右值future转换为shared_future
//拷贝多份供多个线程使用
std::shared_future<int> shared_future2 = shared_future;
//注意: 如果对shared_future进行move 则原shared_future就失效了
std::shared_future<int> shared_future3 = std::move(shared_future);
EXPECT_FALSE(shared_future.valid());
}
//手动进行右值转换
{
std::promise<int> promise;
std::future<int> future = promise.get_future();
std::shared_future<int> shared_future = std::move(future);
EXPECT_FALSE(future.valid());
}
//使用future.share()创建shared_future 并转移所有权
{
std::promise<int> promise;
std::future<int> future = promise.get_future();
std::shared_future<int> shared_future = future.share();
EXPECT_FALSE(future.valid());
}
}
限时等待
等待分为两种,一种是等待一个固定的时间段(duration),一种是等待到某个时刻(time_point)
线程sleep
- std::this_thread,sleep_for(duration),sleep_until(time_point)
条件变量、future限时等待
- wait_for/wait_until
- std::condition_variable
- std::condition_variable_any
- std::future
- std::shared_future
lock限时等待
- try_lock_for/try_lock_until
- std::timed_mutex
- std::recursive_timed_mutex
- std::shared_timed_mutex
- 与上述时间相关的mutex关联的std::unqiue_lock,此外,std::unqiue_lock 也可以在构造器上直接传入duration/time_point
TEST(Test_basic, test_timed_mutex) {
std::timed_mutex mutex;
//启动一个线程持有锁
std::thread t([&mutex] {
mutex.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(200));
mutex.unlock();
});
t.detach();
//sleep100ms 让线程t启动并持有锁
std::this_thread::sleep_for(std::chrono::milliseconds(100));
//等10ms获取锁 获取不到就算了
std::unique_lock lock(mutex, std::chrono::milliseconds(10));
//等10ms获取锁 获取不到就算了 使用try_lock_for
std::unique_lock<std::timed_mutex> lock2(mutex, std::defer_lock);
EXPECT_FALSE(lock.try_lock_for(std::chrono::milliseconds(10)));
EXPECT_FALSE(lock.owns_lock());
}
线程池封装案例
https://github.com/progschj/ThreadPool
class ThreadPool {
public:
ThreadPool(size_t);
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>;
~ThreadPool();
private:
// need to keep track of threads so we can join them
std::vector< std::thread > workers;
// the task queue
std::queue< std::function<void()> > tasks;
// synchronization
std::mutex queue_mutex;
std::condition_variable condition;
bool stop;
};
// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads)
: stop(false)
{
for(size_t i = 0;i<threads;++i)
workers.emplace_back(
[this]
{
for(;;)
{
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->condition.wait(lock,
[this]{ return this->stop || !this->tasks.empty(); });
if(this->stop && this->tasks.empty())
return;
task = std::move(this->tasks.front());
this->tasks.pop();
}
task();
}
}
);
}
// add new work item to the pool
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
using return_type = typename std::result_of<F(Args...)>::type;
auto task = std::make_shared< std::packaged_task<return_type()> >(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
// don't allow enqueueing after stopping the pool
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task](){ (*task)(); });
}
condition.notify_one();
return res;
}
// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for(std::thread &worker: workers)
worker.join();
}
原子操作
什么是原子操作?原子操作是被保证以单独一个事务被执行的操作(参照数据库中的概念),其他线程可以看到原子操作执行之前的系统状态,或者看到原子操作完全执行结束后的系统状态,但不能看到原子操作执行中的系统状态。
原子操作作为无锁编程的基础,为高性能并发提供了可能,但也因其对内存模型的依赖而成为最容易误用的特性之一。与互斥量(如std::mutex)通过阻塞线程实现同步不同,原子操作通过硬件级别的指令保证操作的不可分割性,从而避免了线程上下文切换的开销。然而,这种性能优势是以复杂性为代价的——开发者必须深入理解CPU内存模型和编译器优化才能正确使用。
标准原子类型及可执行的操作
- 内建的标准原子类型,一般针对内建的基本类型,都有对应的原子类型
在头文件 <atomic> 中,有以下内建的标准原子操作类型:



- 各原子类型上可以进行的操作

从C++17开始,所有的原子类型(不包括std::atomic_flag )都包含一个静态常量表达式成员变量,std::atomic::is_always_lock_free 。这个成员变量的值表示在任意给定的目标硬件上,原子类型X是否始终以无锁结构形式实现。如果在所有支持该程序运行的硬件上,原子类型X都以无锁结构形式实现,那么这个成员变量的值就为true;否则为false。
std::atomic_flag
最简单的标准原子类型,也是唯一保证无锁的原子类型,不过操作受限,仅支持test_and_set和clear操作
/**
* 最简单的标准原子类型,表示一个布尔标志。该类型的对象只有两种状态:成立或置零 是唯一保证无锁的原子类型
* 对象只有两个操作:置零、读取原有值并设置标志成立
* 太过于简单 一般不使用
*/
TEST(Test_basic, test_atomic_flag) {
std::atomic_flag flag = ATOMIC_FLAG_INIT; //必须使用ATOMIC_FLAG_INIT初始化
//获取原值并设置标志成立
EXPECT_FALSE(flag.test_and_set());
EXPECT_TRUE(flag.test_and_set());
//置零
flag.clear();
EXPECT_FALSE(flag.test_and_set());
}
使用std::atomic_flag实现自旋锁:
std::atomic_flag flag = ATOMIC_FLAG_INIT; // 必须用此宏初始化
// 实现简单自旋锁
class SpinLock {
private:
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag.test_and_set(std::memory_order_acquire));
}
void unlock() {
flag.clear(std::memory_order_release);
}
};
std::atomic_bool
基于整数的最基本的原子类型,比atomic_flag更灵活
/**
* 相比于atomic_flag atomic_bool更灵活
*/
TEST(Test_basic, test_atomic_bool) {
std::atomic_bool flag(false);
EXPECT_FALSE(flag.load());
flag.store(true);
EXPECT_TRUE(flag.load());
EXPECT_TRUE(flag.is_lock_free());
EXPECT_TRUE(flag.is_always_lock_free);
//交换 返回值为旧值
EXPECT_TRUE(flag.exchange(false));
EXPECT_FALSE(flag.exchange(true));
//cas
bool expected = true; //期望是true
bool desired = false; //如果是true就改成false
bool result = flag.compare_exchange_strong(expected, desired);
//cas成功 result为true 当前值为false
EXPECT_TRUE(result);
EXPECT_TRUE(expected);
EXPECT_FALSE(desired);
//cas失败情况
//现在实际是false
expected = true; //期望是true
desired = false; //如果是true就改成false
result = flag.compare_exchange_strong(expected, desired);
//cas失败 result为false expected被修改为了当前值 当前值为false
EXPECT_FALSE(result);
EXPECT_FALSE(expected);
EXPECT_FALSE(desired);
}
std::atomic<*T> 原子指针
相比于std::atomic_bool类型,增加了指针运算
/**
* 原子类型指针 相比于atomic_bool 增加了一些额外的指针运算操作
*/
TEST(Test_basic, test_atomic_pointer) {
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::atomic<int*> flag;
flag.store(a);
EXPECT_EQ(1, *flag.load());
flag++;
EXPECT_EQ(2, *flag.load());
flag+=2;
EXPECT_EQ(4, *flag.load());
--flag;
EXPECT_EQ(3, *flag.load());
EXPECT_EQ(3, *flag.fetch_add(1)); //返回旧值
EXPECT_EQ(4, *flag.load());
EXPECT_EQ(4, *flag.fetch_sub(1));
EXPECT_EQ(3, *flag.load());
}
标准整数原子类型
与std::atomic_bool类似,不过多了一些运算操作符,如 += 、-= 、|= 、&= 、^= 等
std::atomic<>泛型模板
可以使用模板创建自定义类型的原子类型,不过有一定的要求,且一般编译器无法生成无锁代码,可能不如直接使用std::mutex逻辑更清晰。
TEST(Test_basic, test_atomic_template) {
/*
* 自定义类型必须满足一定的条件:
* - 具备平凡的拷贝运算符
* - 不得含有任何虚函数
* - 不能从虚基类派生
* - 必须由编译器生成隐式拷贝赋值运算符
* - 如果有基类或者非静态数据成员 也必须具备平凡的拷贝运算符
* cas是逐位比较 即使自行定义比较运算符也会被忽略
*/
class MyType {
};
class MyType2 {
int i;
int j;
int x;
};
std::atomic<MyType> flag;
EXPECT_TRUE(flag.is_lock_free());
/*
* 编译器往往没有能力为自定义类型生成无锁代码 不过一般自定义类型提交小于int或者*void 大部分是可以实现无锁的
*/
std::atomic<MyType2> flag2;
EXPECT_FALSE(flag2.is_lock_free());
}
原子操作对应的普通函数
所有原子操作都有对应的普通函数,不同的是,为了兼容c语言,有些参数成员函数传入引用,而普通函数则传入指针。
另外,标准库还对 std::shared_ptr 提供了原子操作
//与原子类型成员函数(方法)等价的普通函数
TEST(Test_basic, test_atomic_function) {
std::atomic_bool flag;
EXPECT_FALSE(flag.load());
EXPECT_FALSE(std::atomic_load(&flag)); //等价的普通函数
//所有原子化操作都有对应的普通函数形式
// std::atomic_store()
// std::atomic_store_explicit() //接收内存次序参数的都是以_explicit结尾
// std::atomic_is_lock_free()
// std::atomic_compare_exchange_strong()
// std::atomic_compare_exchange_strong_explicit()
// std::atomic_compare_exchange_weak()
// std::atomic_compare_exchange_weak_explicit()
// std::atomic_exchange()
// std::atomic_exchange_explicit()
/*
* std::shared_ptr的原子化访问
*/
std::shared_ptr<int> p1 = std::make_shared<int>(1);
//原子读
std::shared_ptr<int> readP1 = std::atomic_load(&p1);
//原子写
std::atomic_store(&p1, std::make_shared<int>(2));
//cas
// std::shared_ptr<int> expected = std::make_shared<int>(2); //cas fail
std::shared_ptr<int> expected = p1; //cas success
std::shared_ptr<int> desired = std::make_shared<int>(3);
bool success = std::atomic_compare_exchange_strong(&p1, &expected, desired);
EXPECT_TRUE(success);
//内部通过自旋锁实现 非无锁设计
EXPECT_FALSE(std::atomic_is_lock_free(&p1));
}
使用建议
何时使用原子操作?
-
适用场景:简单计数器、标志位、无锁数据结构
-
不适用场景:复杂状态转换、需要多操作原子性(此时应使用互斥量)
经验法则:优先使用高级同步原语(如std::mutex、std::condition_variable),仅在性能关键路径且操作简单时才考虑原子操作。
c++11原子操作为并发编程提供了强大的工具,但也要求开发者深入理解内存模型。正确使用原子操作可以显著提升性能,但错误使用会导致难以调试的并发bug。
内存模型
C++ 内存模型是 C++11 标准引入的重要概念,用于规定多线程程序中变量在不同线程之间的可见性与操作顺序,使得并发程序行为可预测。它定义了原子性、可见性和有序性规则。
核心目标是解决并发编程中的两个关键问题:
- 保证多线程访问共享数据时的正确性
- 提供可移植的并发语义,使代码在不同硬件平台(如 x86、ARM)上行为一致。
C++11 之前并没有正式的内存模型,行为全靠平台实现(比如 POSIX 或 Windows API)。C++11 引入了 std::atomic 和 memory_order 来标准化行为。
cpu cache
关于cpu cache需要了解的几个核心的点:
-
为了提高cpu工作效率,cpu不直接操作内存,中间一般会有多级缓存(L1/L2/L3...)
A CPU cache is a hardware cache used by the central processing unit (CPU) of a computer to reduce the average cost (time or energy) to access data from the main memory. A cache is a smaller, faster memory, closer to a processor core, which stores copies of the data from frequently used main memory locations. Most CPUs have different independent caches, including instruction and data caches, where the data cache is usually organized as a hierarchy of more cache levels (L1, L2, L3, L4, etc.). …

-
cache line,cpu与内存之间的缓存以固定大小的数据块进行缓存,称为cache line,或者cache block
Cache line 是 CPU 缓存的基本存储单位,通常由一组连续的内存地址组成。现代处理器的 Cache Line 大小通常为 64 字节,但也可能为 32 字节或 128 字节,具体取决于硬件架构。当 CPU 访问内存时,它不会只加载单个字节,而是会将整个 Cache Line 加载到缓存中,这种机制旨在利用空间局部性,即程序倾向于访问相邻的内存地址。

-
在cpu和L1 cache之间,还有一层缓存StoreBuffer,只负责缓存cpu的写操作,LoadBuffer,负责缓存cpu的读操作
L1 Cache命中的情况下,访问数据一般需要2个指令周期。而且当CPU遭遇写数据cache未命中时,内存访问延迟增加很多。硬件工程师为了追求极致的性能,在CPU和L1 Cache之间又加入一级缓存,我们称之为store buffer。store buffer和L1 Cache还有点区别,store buffer只缓存CPU的写操作。store buffer访问一般只需要1个指令周期,这在一定程度上降低了内存写延迟。不管cache是否命中,CPU都是将数据写入store buffer。store buffer负责后续以FIFO次序写入L1 Cache。

-
cache coherence,每个cpu会在任意时刻将store buffer中的结果写入到cache中,那么多个cpu之间如何保证数据的一致性?
cache coherence 解决了核之间数据可见性以及顺序的问题, 本质上可以把cache当做内存系统的一部分, 有没有cache对各个CPU来说都是一样的, 只要写到了cache(所以可以有很多级cache), 其他核就可以看到该数据, 并且顺序是确定的 – 保证顺序一致性.
对于 Store Buffer 而言,它写到自己的 Cache 就等于写到内存了,其他 CPU 就应该能看见了,至于复杂的一致性问题,交给 MESI.
-
MESI(Modified, Exclusive, Shared, Invalid)是一种cache coherence协议,用于实现缓存一致性。
其定义了四种状态“modified”, “exclusive”, “shared”, “invalid”。
一条cache line无数据就是“invalid”
有数据,且与其他cpu共享就是 “shared”
有数据,与其他cpu不共享,独占的,且自己没修改是 “exclusive”
有数据,与其他cpu不共享,独占的,且自己已修改是 “modified”
除了状态外,还定义了一些操作:
- Read:通知其他处理器和内存,准备读某个地址的cache line数据了
- Read Response:回应 Read 请求,从内存或从其他高速缓存返回请求的数据
- Invalidate:通知其他处理器删除指定内存地址的数据副本(cache line)
- Invalidate Acknowledge:收到 Invalidate 后必须回复,表示已经删除了其缓存的对应数据副本
- Read Invalidate:Read and Invalidate复合消息,主要用于通知其他处理器当前处理器准备更新一个数据了,请求其他处理器删除对应的数据副本,接收该消息的处理器必须回复Read Response和Invalidate Acknowledge
- Writeback:将 Cache line 数据写回到某个内存地址
实际写操作的时候cpu是直接写storebuffer,写完就忙其他事了,等到其他cpu都返回之后,才会把storebuffer中的数据写入缓存。其他cpu也不会立即把本地缓存置为无效并返回,而是写入Invalidate Queue异步进行的,后续cpu异步扫描Invalidate Queue才会实际修改,所以存在脏读可能。
因此,因为这些中间层的存在及异步操作,各cpu看到的数据可能是不一致的。
指令重排
指令重排主要有两种, 一种是compiler在compile-time产生的指令 重排, 一种是CPU在rumtime产生的指令重排, 两者都是为了提高运行效率:
- compile-time: 在编译器重排编译生成指令达到减少使用寄存器load/store的次数, 比 如编译器发现有个寄存器里的值可以先写回到内存里再over write该寄存器的值作它用
- runtime: 在运行时预测(speculate)在同个instruction stream里哪些指令可以先执行, 同时减少类似重复load/store内存或者寄存器, 比如说对分支的speculative execution
不管如何重排, 都不能影响其重排后在单线程里跑出来的结果的正确性, 就是重排后的结果和重排前的结果一样, 指令重排导致原来的内存可见顺序发生了变化, 在单线程执行起来的时候是没有问题的, 但是放到多核/多线程执行的时候就出现问题了, 为了效率引入的额外复杂逻辑的的弊端就出现了。
int msg = 0; // assume operation is atomic
int ready = 0;
------------------------------------------
// thread 1
void foo() {
msg = 10086;
ready = 1;
}
------------------------------------------
// thread 2
void bar() {
if (ready == 1) {
// output may be 0, unexpected
std::cout << msg;
}
}
以上代码的输出并不一定是 10086,也有可能是 0。
thread 1 单线程执行 foo() 先给 msg 赋值和先给 ready 赋值都是最终的结果都是正确的, 符合重排的最基本的原则, 所以在不加任何限制的情况下,编译器生成的汇编可以自由地将 ready 先于 msg 赋值.
但是, 引入(thread 2)多线程之后, 由于业务逻辑上需要有数据的依赖关系, ready 之后才能输出 msg 但是 thread 2并不知道 ready 和 msg 有什么顺序关系, 只负责自己的 bar() 执行即可, 在 thread 2看起来自己的执行也是正确的, 但这个时候运行起来就有”bug”了.
可以使用 asm volatile ("" ::: "memory") 阻止编译器层面的代码重排。
- 编译器指令重排的例子
// cpp code | ;asm code
|
int a = 0; |
int b = 0; |
|
void foo() { | foo():
a = b + 1; | mov eax, DWORD PTR b[rip]
b = 5; | mov DWORD PTR b[rip], 5
} | add eax, 1
| mov DWORD PTR a[rip], eax
| ret
以上代码使用x86-64 gcc 8.2编译, 编译选项-O2, 其他architecture的CPU代码也许不一 样, 在我理解, 编译器的这个”优化”看起来是很合理的, 把同个内存地址的都先处理了, 以防后后续还有对该内存的处理从而需要额外使用寄存器来操作该内存(之前的寄存器可能被覆盖了).
我们可以通过强制约束编译器来get rid of this kind of reordering. 在两个赋值语句之 间添加一个”compile-time memory order constraint”.
// cpp code | ;asm code
|
int a = 0; |
int b = 0; |
|
void foo() { | foo():
a = b + 1; | mov eax, DWORD PTR b[rip]
std::atomic_thread_fence( | add eax, 1
std::memory_order_release); | mov DWORD PTR a[rip], eax
b = 5; | mov DWORD PTR b[rip], 5
} | ret
可以看到在我们添加std::atomic_thread_fence(std::memory_order_release)限制之后, 生成两个赋值的汇编代码就按照我们的cpp代码顺序进行了.
这里想说的是, compile-time reordering是我们能够控制的, 一般情况下编译器的优化 默认会进行一些reordering, 如果没有说明特殊的需求, 不需要显式限制编译器的优化.
还有一些冗余赋值优化:
// cpp code | ; asm code
int a = 0; |
int b = 0; |
void foo() { | foo():
a = 1; | mov DWORD PTR b[rip], 2
b = 2; | mov DWORD PTR a[rip], 3
a = 3; | ret
} |
因此,由于指令可能重排,不同cpu之间看到的数据的顺序也可能是不一样的。
Acquire 和 Release 语义
Release :将当前对内存的修改发布出去,使修改对其他线程可见。
- 对指令重排的限制:在此之前的所有指令,都不能重排到这个指令之后
- 刷新StoreBuffer,将其内容写到自己的cache里(由缓存一致性协议保证其他线程可见)
Acquire:从其他线程获取对内存的修改,使他们的修改对我们可见。
- 对指令重排的限制:在此之后的所有指令,都不能重排到这个指令之前
- 处理 Invalidate queue 中的消息,逐出对应 Cache line,当需要时,从其他 CPU 获取新的(“Read”)
原子操作的内存次序
C++11定义了六种内存次序(memory_order),来实现内存模型中不同的语义:
enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
};
对内存的操作可以分为三种:读(load)、写(store)、读改写(cas、fetch_add等),每种操作可以使用的次序如下:
- 读:std::memory_order_relaxed、std::memory_order_acquire、std::memory_order_seq_cst
- 写:std::memory_order_relaxed、std::memory_order_consume、std::memory_order_release、std::memory_order_seq_cst
- 读改写:std::memory_order_relaxed、std::memory_order_consume、std::memory_order_acquire、std::memory_order_release、std::memory_order_acq_rel、std::memory_order_seq_cst
六种内存次序可以实现三种顺序模型:
-
先后一致次序:实现同步, 且保证全局顺序一致 (single total order) 的模型. 是一致性最强的模型, 也是默认的顺序模型(std::memory_order_seq_cst)
-
获取-是否次序:实现同步, 但不保证保证全局顺序一致的模型(std::memory_order_consume、std::memory_order_acquire、std::memory_order_release、std::memory_order_acq_rel)
-
宽松次序:不能实现同步, 只保证原子性的模型(std::memory_order_relaxed)
在不同的cpu架构上,以上三种模型可能会有不同的允许开销,c++提供了上述各种内存序模型,资深程序员可以自由选用,籍此充分利用更为细分的次序关系,从而提升程序性能。
总结下内存次序之间的约束强度关系:
/----------> release ----------\
/ \
relaxed ---> ---> acq_res ---> seq_cst
\ /
\---> consume ---> acquire ----/
an arrow A -> B means constraint of B is stronger than A
-
memory_order_relaxed:仅保证操作本身的原子性,不提供任何同步或顺序约束,所有操作均可使用。
适用于纯计数器等场景:
std::atomic<int> counter(0); counter.fetch_add(1, std::memory_order_relaxed); // 仅保证计数正确,不影响其他操作顺序一个错误使用的例子:
std::atomic<bool> x, y; std::atomic<int> z; //thread 1 void write_x_then_y() { x.store(true, std::memory_order_relaxed); // 1 y.store(true, std::memory_order_relaxed); // 2 } //thread 2 void read_y_then_x() { while (!y.load(std::memory_order_relaxed)); // 3 if (x.load(std::memory_order_relaxed)) { //4 y为true x不一定是true 宽松次序只保证原子性 ++z; } } void TestOrderRelaxed() { x = false; y = false; z = 0; std::thread t1(write_x_then_y); std::thread t2(read_y_then_x); t1.join(); t2.join(); assert(z.load() != 0); // 5 可能触发断言 z可能是0也可能是1 } -
memory_order_acquire、memory_order_release:分别用于读写,可以实现同步
-
release:当前线程的所有写操作在其他线程对同一原子变量的acquire操作前可见,适用于写操作,对读操作无意义
-
acquire:可见所有在release操作前的写操作,适用于读操作,对写操作无意义
典型应用是生产者-消费者模型:
std::atomic<bool> data_ready(false); int shared_data; // 生产者线程 void producer() { shared_data = 42; // 1. 写入数据 data_ready.store(true, std::memory_order_release); // 2. 发布信号 } // 消费者线程 void consumer() { while (!data_ready.load(std::memory_order_acquire)); // 3. 获取信号 assert(shared_data == 42); // 4. 安全读取数据,保证看到42 }
-
-
std::memory_order_acq_rel:前面两者的结合,既release又acquire,适用于读改写操作,也可以用于读(release无意义)、写操作(acquire无意义)
-
memory_order_seq_cst,最严格的内存次序,保证所有线程看到的操作顺序一致,如同在单个全局序列中执行。但性能开销最大,仅在需要全局同步时使用,原子操作默认的次序。
-
std::memory_order_consume:acquire 内存模型的进一步细化,它仅限制依赖变量的顺序,只保证依赖于该原子加载结果的那些操作不会被重排到加载之前,acquire则保证加载之后的所有操作都不会被重排到加载之前。由于其复杂的语义和编译器实现的困难,它通常不被推荐使用或被降级为aquire。c++17明确建议不予使用。
内存屏障(栅栏)
强制施加内存次序,却无须改动任何数据,通常与服从memory_order_relaxed次序的原子操作组合使用,实现同步关系。针对不同变量上的宽松操作,编译器或硬件往往可以自主对其进行重新编排,栅栏限制了这种重新编排。
std::atomic<bool> x, y;
std::atomic<int> z;
//thread 1
void write_x_then_y() {
x.store(true, std::memory_order_relaxed); // 1
std::atomic_thread_fence(std::memory_order_release); //释放栅栏 发布最新的修改
y.store(true, std::memory_order_relaxed); // 2
}
//thread 2
void read_y_then_x() {
while (!y.load(std::memory_order_relaxed)); // 3
std::atomic_thread_fence(std::memory_order_acquire); //获取栅栏 获取最新的修改
if (x.load(std::memory_order_relaxed)) { //4
++z;
}
}
void TestOrderRelaxed() {
x = false;
y = false;
z = 0;
std::thread t1(write_x_then_y);
std::thread t2(read_y_then_x);
t1.join();
t2.join();
assert(z.load() != 0); // 5 不会触发断言
}
参考
- 《C++并发编程实战》第二版
- 原子操作和内存模型, https://gitbookcpp.llfc.club/sections/cpp/concurrent/concpp11.html
- 原子操作与内存模型/序/屏障 (Atomic operation & Memory model), https://zhuanlan.zhihu.com/p/611868395
- memory ordering,http://gavinchou.github.io/summary/c++/memory-ordering/
其他话题
-
设计基于锁的并发数据结构
-
设计无锁的数据结构
-
高级线程管理:线程池设计,线程中断设计
本文来自博客园,作者:Bingmous,转载请注明原文链接:https://www.cnblogs.com/bingmous/p/19069899
浙公网安备 33010602011771号