1. 线程基础 1.1 线程发起 线程发起就是指启动一个线程,C++11标准统一了线程操作,可以在定义一个线程变量后,该变量启动线程执行回调逻辑。
1 2 3 4 5 6 7 8 void thead_work1 (std::string str) { std::cout << "str is " << str << std::endl; } int main () { std::string hellostr = "hello world!" ; std::thread t1 (thead_work1, hellostr) ; t1.join (); }
在上面程序中,如果没有t1.join()
;或用std::this_thread::sleep_for(std::chrono::seconds(1));
代替,执行该程序都会报错。需要调用t1.join();
来等t1子线程执行完了,主线程才往下执行。
1.2 仿函数作为参数 用仿函数作为参数传递给线程时,也可以当作线程的回调函数来使用。下面程序中,第1种执行方法,编译器会将t1当成一个函数对象, 返回一个std::thread类型的值, 即函数的参数被视为一个函数指针了,所以程序是有问题的,可以通过第2和第3种方式执行,用()或{}避免将background_task()视为一个函数指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class background_task {public : void operator () () { std::cout << "background_task called" << std::endl; } }; int main () { std::thread t2 ((background_task())) ; t2.join (); std::thread t3{ background_task () }; t3.join (); }
1.3 lambda表达式 lambda表达式也可以作为线程的参数传递给thread
1 2 3 4 5 6 7 int main{ std::string hellostr = "hello world!" ; std::thread t4 ([](std::string str) { std::cout << "str is " << str << std::endl; }, hellostr) ; t4.join (); }
1.4 detach使用 detach允许子线程采用分离的方式在后台独自运行,但下面的程序仍然有隐患。因为some_local_state
是局部变量, 当oops
调用结束后局部变量some_local_state
就可能被释放了,而子线程还在detach后台运行,容易出现崩溃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 struct func { int & _i; func (int & i): _i(i){} void operator () () { for (int i = 0 ; i < 3 ; i++) { _i = i; std::cout << "_i is " << _i << std::endl; std::this_thread::sleep_for (std::chrono::seconds (1 )); } } }; void oops () { int some_local_state = 0 ; func myfunc (some_local_state) ; std::thread functhread (myfunc) ; functhread.detach (); } int main () { oops (); std::this_thread::sleep_for (std::chrono::seconds (1 )); }
所以当我们在子线程中使用主线程的一些局部变量,或通过引用或指针的方式使用了一些函数的局部变量,一定要关注这些局部变量是否会被释放掉。解决该方面问题的一些方法:
通过智能指针传递参数,因为引用计数会随着赋值增加,可保证局部变量在使用期间不被释放,这也就是伪闭包策略。
将局部变量的值作为参数传递,这么做需要局部变量有拷贝复制的功能,而且拷贝耗费空间和效率。
将线程运行的方式用join代替detach,这样能保证局部变量被释放前线程已经运行结束。但是这么做可能会影响运行逻辑。
1.5 异常处理 当我们启动一个线程后,如果主线程产生崩溃,会导致子线程也会异常退出,就是调用terminate,如果子线程在进行的是一些重要的操作,那么丢失这些信息是很危险的。所以常用的做法是捕获异常,即在主线程出现异常情况下需要保证子线程稳定运行结束后,主线程才抛出异常结束运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 void catch_exception () { int some_local_state = 0 ; func myfunc (some_local_state) ; std::thread functhread{ myfunc }; try { std::this_thread::sleep_for (std::chrono::seconds (1 )); }catch (std::exception& e) { functhread.join (); throw ; } functhread.join (); }
2. 线程管控 2.1 线程归属权 众所周知线程可以通过detach在后台运行或者通过join让开辟这个线程的父线程等待该线程完成。但每个线程都应该有其归属权,也就是归属给某个变量管理:
下面程序启动了一个线程,t1
是线程的变量,这里可以理解为在定义完线程变量,系统就会帮我们去分配线程资源,让线程运行起来,线程就归属t1
变量管理。
1 2 3 4 5 6 7 8 void some_function () { while (true ) { std::this_thread::sleep_for (std::chrono::seconds (1 )); } } int main () { std::thread t1 (some_function) ; }
我们知道t1
是一个线程变量,管理一个线程,该线程执行some_function()
。对于std::thread
C++ 不允许其执行拷贝构造 和拷贝赋值 ,所以只能通过移动和局部变量返回的方式将线程变量管理的线程转移给其他变量管理。在c++中,像std::mutex
, std::ifstream
, std::unique_ptr
都是这样的类型。
在下面程序中,定义了一个t1
变量管理一个线程,将t1
管理的线程通过move的方式转移给了t2
,move过后,t1
变量就没有绑定线程了,即无效变量了。虽然t1
无效了,但还可以继续赋值,通过t1 = std::thread(some_other_function)
给t1
绑定新的线程。为什么这种赋值方式可以?因为std::thread
返回的是一个局部变量,且是右值,右值赋给t1
,调用的就是thread的移动赋值函数
,这种方式是可以的,t1
就绑定了新的线程。如果这时再创建一个线程变量t3
,将t2
的线程转移给t3
来用,t3
再转移给t1
,这时就会出现崩溃了。因为t1
已经绑定了线程,再对起进行赋值,就会造成它原来的线程强行终止,触发线程内部terminate
函数,该terminate
就会引发程序的崩溃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void some_function () { while (true ) { std::this_thread::sleep_for (std::chrono::seconds (1 )); } } void some_other_function () { while (true ) { std::this_thread::sleep_for (std::chrono::seconds (1 )); } } std::thread t1 (some_function) ; std::thread t2 = std::move (t1); t1 = std::thread (some_other_function); std::thread t3; t3 = std::move (t2); t1 = std::move (t3); std::this_thread::sleep_for (std::chrono::seconds (2000 ));
2.2 局部变量返回值 对于返回一个局部变量给调用者,是当函数返回一个类类型的局部变量时会先调用移动构造,如果没有移动构造再调用拷贝构造。 所以对于一些没有拷贝构造但是实现了移动构造的类类型也支持通过函数返回局部变量。 在 C++11 之后,编译器会默认使用移动语义来提高性能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class TestCopy {public : TestCopy (){} TestCopy (const TestCopy& tp) { std::cout << "Test Copy Copy " << std::endl; } TestCopy (TestCopy&& cp) { std::cout << "Test Copy Move " << std::endl; } }; TestCopy TestCp () { TestCopy tp; return tp; }
返回值优化(RVO)
:C++ 编译器通常会进行返回值优化(RVO),甚至在有移动构造函数时也可能跳过移动构造 ,直接在调用者的内存空间中构造对象,以进一步减少性能开销。
2.3 容器存储 容器存储线程时,比如vector
,如果用push_back
操作势必会调用std::thread
,又因为std::thread
没有拷贝构造函数,这样会引发编译错误。而采用emplace方式,可以直接根据线程构造函数需要的参数来构造(比如说threads容器里存的thread类型的变量,通过emplace方法,传入的参数就可以根据thread构造函数时需要的参数,参数就会生成一个右值,存到emplace里),这样就避免了调用thread的拷贝构造函数。
1 2 3 4 5 6 7 8 9 10 11 12 void use_vector () { std::vector<std::thread> threads; for (unsigned i = 0 ; i < 10 ; ++i) { threads.emplace_back (param_function, i); } for (auto & entry : threads) { entry.join (); } }
2.4 其它函数 std::thread::hardware_concurrency()
函数:它的返回值是一个指标,表示程序在各次运行中可真正并发的线程数量。
get_id()
函数:获取线程id。
3. 互斥与死锁 3.1 锁的使用 对于一些共享的数据,可以通过mutex
对共享数据进行加锁,防止多线程访问共享区造成数据不一致问题。
在下面程序中,初始化了一个共享变量shared_data
,然后定义了一个互斥量std::mutex
,接下来启动了两个线程,一个是use_lock
函数增加数据,另一个lambda表达式减少数据。从最后执行的结果可以看到两个线程对于共享数据的访问是独占的,单位时间片只有一个线程访问并输出日志。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 std::mutex mtx1; int shared_data = 100 ; void use_lock () { while (true ) { mtx1.lock (); shared_data++; std::cout << "current thread is " << std::this_thread::get_id () << std::endl; std::cout << "share data is " << shared_data << std::endl; mtx1.unlock (); std::this_thread::sleep_for (std::chrono::microseconds (10 )); } } void test_lock () { std::thread t1 (use_lock) ; std::thread t2 ([]() { while (true ) { mtx1.lock(); shared_data--; std::cout << "current thread is " << std::this_thread::get_id() << std::endl; std::cout << "share data is " << shared_data << std::endl; mtx1.unlock(); std::this_thread::sleep_for(std::chrono::microseconds(10 )); } }) ; t1.join (); t2.join (); } int main () { test_lock (); }
3.2 lock_guard的使用 lock_guard
可以自动地加锁和解锁,参数只需要传入互斥量即可,就没有必要手动的去加锁和解锁了。因为lock_guard解锁是要遇到右括号才自动解锁,所以需要把与访问共享区域有关的代码放入一个新的{}里面执行。
1 2 3 4 5 6 7 8 9 10 11 void use_lock () { while (true ) { { std::lock_guard<std::mutex> lock (mtx1) ; shared_data++; std::cout << "current thread is " << std::this_thread::get_id () << std::endl; std::cout << "sharad data is " << shared_data << std::endl; } std::this_thread::sleep_for (std::chrono::microseconds (10 )); } }
3.3 保证数据安全 有时候我们可以将对共享数据的访问和修改聚合到一个函数,在函数内加锁保证数据的安全性。但是对于读取类型的操作,即使读取函数是线程安全的,但是返回值抛给外边使用,可能就会存在不安全性。
对于下面这个程序,empty()函数内部对栈进行访问的时候,用了加锁,是安全的,但当将是否为空的结果传到外部后,外部将结果存到队列中,没有及时用。如果这段期间栈有所变化,则会造成程序崩溃。总的来说,就是empty()在返回true或false时,使用这个bool时机的一个问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 template <typename T>class threadsafe_stack1 {private : std::stack<T>data; mutable std::mutex m; public : threadsafe_stack1 () {} threadsafe_stack1 (const threadsafe_stack1& other) { std::lock_guard<std::mutex>lock (other.m); data = other.data; } threadsafe_stack1& operator = (const threadsafe_stack1&) = delete ; void push (T new_value) { std::lock_guard<std::mutex>lock (m); data.push (std::move (new_value)); } T pop () { std::lock_guard<std::mutex>mutex (m); auto element = data.top (); data.pop (); return element; } bool empty () const { std::lock_guard<std::mutex>lock (m); return data.empty (); } }; void test_threadsafe_stack1 () { threadsafe_stack1<int >safe_stack; safe_stack.push (1 ); std::thread t1 ([&safe_stack]() { if (!safe_stack.empty()) { std::this_thread::sleep_for(std::chrono::seconds(1 )); safe_stack.pop(); } }) ; std::thread t2 ([&safe_stack]() { if (!safe_stack.empty()) { std::this_thread::sleep_for(std::chrono::seconds(1 )); safe_stack.pop(); } }) ; t1.join (); t2.join (); }
3.4 死锁 死锁一般是由于调运顺序不一致导致的,比如两个线程循环调用。当线程1先加锁A,再加锁B,而线程2先加锁B,再加锁A。那么在某一时刻就可能造成死锁,因为它们彼此都想要占用对方的锁,又不释放自己占有的锁,这样就导致了死锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 std::mutex t_lock1; std::mutex t_lock2; int m_1 = 0 ;int m_2 = 1 ;void dead_lock1 () { while (true ) { std::cout << "dead_lock1 begin " << std::endl; t_lock1.lock (); m_1 = 1024 ; t_lock2.lock (); m_2 = 2048 ; t_lock2.unlock (); t_lock1.unlock (); std::cout << "dead_lock1 end" << std::endl; } } void dead_lock2 () { while (true ) { std::cout << "dead_lock2 begin " << std::endl; t_lock1.lock (); m_1 = 1024 ; t_lock2.lock (); m_2 = 2048 ; t_lock2.unlock (); t_lock1.unlock (); std::cout << "dead_lock2 end" << std::endl; } } void test_dead_lock () { std::thread t1 (dead_lock1) ; std::thread t2 (dead_lock2) ; t1.join (); t2.join (); }
3.5 层级锁 现实开发中常常很难规避同一个函数内部加多个锁的情况,我们要尽可能避免循环加锁。所以可以自定义一个层级锁,它是一种用于避免死锁的策略,它通过为锁分配优先级来减少死锁的可能性。在这种策略中,每个锁都被赋予一个唯一的优先级,并且规定只能按照优先级顺序获取锁,这样就保证实际项目中对多个互斥量加锁时是有序的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 class hierarchical_mutex {public : explicit hierarchical_mutex (unsigned long value) :_hierarchy_value(value), _previous_hierarchy_value(0 ) { } hierarchical_mutex (const hierarchical_mutex&) = delete ; hierarchical_mutex& operator =(const hierarchical_mutex&) = delete ; void lock () { check_for_hierarchy_violation (); _internal_mutex.lock (); update_hierarchy_value (); } void unlock () { if (_this_thread_hierarchy_value != _hierarchy_value) { throw std::logic_error ("mutex hierarchy violated" ); } _this_thread_hierarchy_value = _previous_hierarchy_value; _internal_mutex.unlock (); } bool try_lock () { check_for_hierarchy_violation (); if (!_internal_mutex.try_lock ()) { return false ; } update_hierarchy_value (); return true ; } private : std::mutex _internal_mutex; unsigned long const _hierarchy_value; unsigned long _previous_hierarchy_value; static thread_local unsigned long _this_thread_hierarchy_value; void check_for_hierarchy_violation () { if (_this_thread_hierarchy_value <= _hierarchy_value) { throw std::logic_error ("mutex hierarchy violated" ); } } void update_hierarchy_value () { _previous_hierarchy_value = _this_thread_hierarchy_value; _this_thread_hierarchy_value = _hierarchy_value; } }; thread_local unsigned long hierarchical_mutex::_this_thread_hierarchy_value(ULONG_MAX);void test_hierarchy_lock () { hierarchical_mutex hmtx1 (1000 ) ; hierarchical_mutex hmtx2 (500 ) ; std::thread t1 ([&hmtx1, &hmtx2]() { hmtx1.lock(); hmtx2.lock(); hmtx2.unlock(); hmtx1.unlock(); }) ; std::thread t2 ([&hmtx1, &hmtx2]() { hmtx2.lock(); hmtx1.lock(); hmtx1.unlock(); hmtx2.unlock(); }) ; t1.join (); t2.join (); }
主要原理就是将当前锁的权重保存在线程变量中,这样该线程再次加锁时判断线程变量的权重和锁的权重是否大于,如果满足条件则继续加锁。
3.6 unique_lock unique_lock
和lock_guard
基本用法相同,构造时默认加锁,析构时默认解锁,但unique_lock
有个好处就是可以手动解锁。这一点尤为重要,方便我们控制锁住区域的粒度(加锁的范围大小),也能支持和条件变量配套使用。
1 2 3 4 5 6 7 8 9 10 std::mutex mtx; int shared_data = 0 ;void use_unique () { std::unique_lock<std::mutex> lock (mtx) ; std::cout << "lock success" << std::endl; shared_data++; lock.unlock (); }
同时,还可以通过unique_lock
的owns_lock
来判断是否持有锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void owns_lock () { std::unique_lock<std::mutex> lock (mtx) ; shared_data++; if (lock.owns_lock ()) { std::cout << "owns lock" << std::endl; } else { std::cout << "doesn't own lock" << std::endl; } lock.unlock (); if (lock.owns_lock ()) { std::cout << "owns lock" << std::endl; } else { std::cout << "doesn't own lock" << std::endl; } }
unique_lock
也可以完成延迟加锁。这里的延迟加锁是指在 std::unique_lock
对象创建时不立即锁定互斥量,而是在之后显式调用 lock.lock()
时才锁定。因此,这个延迟加锁的时间并不是指一个具体的时间段,而是指在 std::unique_lock
对象创建后到显式调用 lock.lock()
之间的这段时间。
1 2 3 4 5 void defer_lock () { std::unique_lock<std::mutex> lock (mtx, std::defer_lock) ; lock.lock (); lock.unlock (); }
下面综合运用owns_lock
和defer_lock
来实现一个例子,下面这个程序中会发生阻塞,阻塞在24行的lock.lock()
。因为主线程在初始化时就加锁了,未释放,等待子线程执行完了自动释放;而子线程虽然是延迟加锁,但到了下面显示加锁时,加不了锁,就会阻塞在这里。因此就导致整个程序卡住。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 void use_own_defer () { std::unique_lock<std::mutex> lock (mtx) ; if (lock.owns_lock ()) { std::cout << "Main thread has the lock." << std::endl; } else { std::cout << "Main thread does not have the lock." << std::endl; } std::thread t ([]() { std::unique_lock<std::mutex> lock(mtx, std::defer_lock); if (lock.owns_lock()) { std::cout << "Thread has the lock." << std::endl; } else { std::cout << "Thread does not have the lock." << std::endl; } lock.lock(); if (lock.owns_lock()) { std::cout << "Thread has the lock." << std::endl; } else { std::cout << "Thread does not have the lock." << std::endl; } lock.unlock(); }) ; t.join (); }
和lock_guard
一样,unique_lock
也支持领养锁。下面的程序中,首先通过mtx互斥量进行加锁,然后创建了一个lock对象来接管这个锁,此时mtx就无效了,后面就可以通过手动的lock对象来释放锁,但通过mtx.unlock()
来释放锁就会报错。如果开始没有先通过mtx来进行加锁,后面通过lock对象来接管锁也会报错,因为锁还没有加锁,接管不了。
1 2 3 4 5 6 7 8 9 10 11 12 std::mutex mtx; void use_own_adopt () { mtx.lock (); std::unique_lock<std::mutex> lock (mtx, std::adopt_lock) ; if (lock.owns_lock ()) { std::cout << "owns lock" << std::endl; } else { std::cout << "does not have the lock" << std::endl; } lock.unlock (); }
需要注意的是,一旦mutex
被unique_lock
管理,加锁和释放的操作就交给unique_lock
,不能调用mutex
加锁和解锁,因为锁的使用权已经交给unique_lock
了。
1 2 3 4 5 6 7 8 9 std::mutex mtx1; std::mutex mtx2; void safe_swap2 () { std::unique_lock<std::mutex> lock1 (mtx1, std::defer_lock) ; std::unique_lock<std::mutex> lock2 (mtx2, std::defer_lock) ; std::lock (lock1, lock2); }
众所周知的mutex
是不支持移动和拷贝的,但是unique_lock
支持移动,当一个mutex
被转移给unique_lock
后,可以通过unique_ptr
转移其归属权。
在下面程序中,get_lock()
首先定义了一个局部的lock,执行相应操作后,返回该lock,因为到达了作用域末尾,所以自动解锁了。虽然解锁了,但该lock已经作为局部变量被返回了,在use_return
函数中,因为返回的不是引用类型,所以先考虑拷贝构造和拷贝赋值,因为unique_lock
没有这两种拷贝,所以就只能用移动构造,会把lock里所有的管理权交给use_return
里的lock对象。这意味着互斥量mtx在 use_return
函数中的lock对象被销毁之前不会被解锁。
1 2 3 4 5 6 7 8 9 10 11 std::mutex mtx; std::unique_lock <std::mutex> get_lock () { std::unique_lock<std::mutex> lock (mtx) ; shared_data++; return lock; } void use_return () { std::unique_lock<std::mutex> lock (get_lock()) ; shared_data++; }
4. 线程安全的单例模式 4.1 局部静态变量 众所周知当一个函数中定义一个局部静态变量,那么这个局部静态变量只会初始化一次,就是在这个函数第一次调用的时候,以后无论调用几次这个函数,函数内的局部静态变量都不再初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Single2 {private : Single2 (){} Single2 (const Single2&) = delete ; Single2& operator =(const Single2&) = delete ; public : static Single2& GetInst () { static Single2 single; return single; } }; void test_single2 () { std::cout<<"s1 addr is " <<&Single2::GetInst ()<<std::endl; std::cout<<"s2 addr is " <<&Single2::GetInst ()<<std::endl; }
上述版本的单例模式在C++11 以前存在多线程不安全的情况,编译器可能会初始化多个静态变量。但是C++11推出以后,各厂商优化编译器,能保证线程安全。尽管是多个线程,多核机器实现,只要调用GetInst()
接口,里面调用生成的局部变量都是统一的。
在C++11 推出以前,局部静态变量的方式实现单例存在线程安全问题,所以部分人推出了以下方案实现:
4.2 饿汉模式 饿汉模式就是指定义类的时候就创建了单例对象,创建出来后,什么时候用,时候什么就调用静态的成员函数(得到单例对象),且在多线程的场景下,饿汉模式是没有线程安全问题的(多线程可以同时访问这个单例的对象)。但这种方法的缺点就是浪费空间资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class TaskQueue { public : TaskQueue (const TaskQueue& t) = delete ; TaskQueue& operator = (const TaskQueue& t) = delete ; static TaskQueue* getInstance () { return m_taskQ; } void print () { cout << "我是单例对象的一个成员函数..." << endl; } private : TaskQueue () = default ; static TaskQueue* m_taskQ; }; TaskQueue* TaskQueue::m_taskQ = new TaskQueue;
4.3 懒汉模式 懒汉模式就是指什么时候使用这个单例对象,在使用的时候再去创建对应的实例,这样就解决了饿汉模式下的空间资源浪费的问题。但在多线程的场景下,懒汉模式是有线程安全问题的(在多线程情况下,若干个线程同时访问单例的实例,会出问题)。 解决方式:通过互斥锁,阻塞线程,依次访问这个单例对象,就可以避免在懒汉模式下多线程同时访问单例对象而创建出多个类的实例这种问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class SinglePointer { private : SinglePointer (){} SinglePointer (const SinglePointer&) = delete ; SinglePointer& operator =(const SinglePointer&) = delete ; public : static SinglePointer* GetInst () { if (single != nullptr ) { return single; } s_mutex.lock (); if (single != nullptr ) { s_mutex.unlock (); return single; } single = new SinglePointer (); s_mutex.unlock (); return single; } private : static SinglePointer* single; static std::mutex s_mutex; }; SinglePointer* SinglePointer::single = nullptr ; std::mutex SinglePointer::s_mutex;
5. 条件变量 在下面程序中,线程1负责打印1后++,线程2负责打印2后–,但当线程1打印完1后,如果2还没有打印,那么它将睡眠,线程2也是相同的情况,所以就导致打印时虽然是12…12这样打印的,但中间互发生停顿,这样就浪费就cpu的时间片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 int num = 1 ;std::mutex mtx_num; void PoorImpleman () { std::thread t1 ([]() { for (;;) { { std::lock_guard<std::mutex> lock(mtx_num); if (num == 1 ) { std::cout << "thread A print 1....." << std::endl; num++; continue ; } } std::this_thread::sleep_for(std::chrono::milliseconds(500 )); } }) ; std::thread t2 ([]() { for (;;) { { std::lock_guard<std::mutex> lock(mtx_num); if (num == 2 ) { std::cout << "thread B print 2....." << std::endl; num--; continue ; } } std::this_thread::sleep_for(std::chrono::milliseconds(500 )); } }) ; t1.join (); t2.join (); }
下面就可以通过条件变量的方式实现,就是当线程1在执行打印完1后,会将线程2唤醒;而当线程2打印完线程2后,有会唤醒线程1。这样执行的结果就不会出现中间停顿的现象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 int num = 1 ;std::mutex mtx_num; std::condition_variable cvA; std::condition_variable cvB; void ResonableImplemention () { std::thread t1 ([]() { for (;;) { std::unique_lock<std::mutex> lock(mtx_num); cvA.wait(lock, []() { return num == 1 ; }); num++; std::cout << "thread A print 1....." << std::endl; cvB.notify_one(); } }) ; std::thread t2 ([]() { for (;;) { std::unique_lock<std::mutex> lock(mtx_num); while (num!=2 ){ cvB.wait(lock); } num--; std::cout << "thread B print 2....." << std::endl; cvA.notify_one(); } }) ; t1.join (); t2.join (); }
6. 并发三剑客future, promise和async 6.1 async与future std::async
是一个用于异步执行函数的模板函数,它返回一个 std::future
对象,该对象用于获取函数的返回值。
在下面这个程序中,定义了一个需要耗费一定时间的任务函数,在主函数中,先启动一个异步执行的任务,通过future
定义的变量来接收异步返回的一个结果,并可以使用get()
来获取该结果。如果该异步执行的函数还没有完成,get()
就会一直阻塞着,但由于任务函数是子线程异步执行的,所以在执行get()
这期间,可以执行其它内容的程序,当需要调用异步任务的结果时,再直接通过get()
获取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 std::string fetchDataFromDB (std::string query) { std::this_thread::sleep_for (std::chrono::seconds (5 )); return "Data: " + query; } int main () { std::future<std::string> resultFromDB = std::async (std::launch::async, fetchDataFromDB, "Data" ); std::cout << "Doing something else..." << std::endl; std::string dbData = resultFromDB.get (); std::cout << dbData << std::endl; return 0 ; }
启动策略:std::async
函数可以接受几个不同的启动策略,这些策略在std::launch
枚举中定义。除了上述使用的std::launch::async
异步执行 之外,还有以下启动策略:
std::launch::deferred
:延迟执行,这种策略意味着任务将在调用std::future::get()
或std::future::wait()
函数时才执行。即任务将在需要结果时同步执行 。
std::launch::async | std::launch::deferred
:这种策略是上面两个策略的组合。任务可以在一个单独的线程上异步执行,也可以延迟执行,具体取决于实现。默认情况下,std::async
使用的是这种策略,但需要注意的是,不同的编译器和操作系统可能会有不同的默认行为。
6.2 future的wait和get std::future::get()
和 std::future::wait()
是 C++ 中用于处理异步任务的两个方法。
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
的wait_for()
或wait_until()
方法来检查异步操作是否已完成。这些方法返回一个表示操作状态的std::future_status
值。
6.3 任务和future关联 std::packaged_task
和std::future
是C++11中引入的两个类,它们用于处理异步任务的结果。
std::packaged_task
是一个可调用目标,它包装了一个任务,该任务可以在另一个线程上运行。它可以捕获任务的返回值或异常,并将其存储在std::future
对象中,以便以后使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int my_task () { std::this_thread::sleep_for (std::chrono::seconds (5 )); std::cout << "my task run 5 s" << std::endl; return 42 ; } void use_package () { std::packaged_task<int () > task (my_task) ; std::future<int > result = task.get_future (); std::thread t (std::move(task)) ; t.detach (); int value = result.get (); std::cout << "The result is: " << value << std::endl; }
6.4 promise 用法 C++11引入了std::promise
和std::future
两个类,用于实现异步编程。std::promise
用于在某一线程中设置某个值或异常,而std::future
则用于在另一线程中获取这个值或异常。
在下面的程序中,首先创建了一个std::promise<int>
对象,然后通过调用get_future()
方法获取与之相关联的std::future<int>
对象。然后,我们在新线程中通过调用set_value()
方法设置promise
的值,并在主线程中通过调用fut.get()
方法获取这个值。注意,在调用fut.get()
方法时,如果promise
的值还没有被设置,则该方法会阻塞当前线程,直到值被设置为止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void set_value (std::promise<int > prom) { std::this_thread::sleep_for (std::chrono::seconds (5 )); prom.set_value (10 ); std::cout<<"promise set value success" <<std::endl; } int main () { std::promise<int > prom; std::future<int > fut = prom.get_future (); std::thread t (set_value, std::move(prom)) ; std::cout << "Waiting for the thread to set the value...\n" ; std::cout << "Value set by the thread: " << fut.get () << '\n' ; t.join (); return 0 ; }
除了set_value()
方法外,std::promise
还有一个set_exception()
方法,用于设置异常。该方法接受一个std::exception_ptr
参数,该参数可以通过调用std::current_exception()
方法获取。在下面这个程序中,子线程是抛出异常,然后自己又捕获异常,捕获异常就是将异常值写入到prom里,这样主线程就可以通过fut.get()
方法得到子线程的异常值了。但要注意的是,主线程必须要用try-catch
来捕获该异常,不然系统会崩溃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void set_exception (std::promise<void > prom) { try { throw std::runtime_error ("An error occurred!" ); } catch (...) { prom.set_exception (std::current_exception ()); } } int main () { std::promise<void > prom; std::future<void > fut = prom.get_future (); std::thread t (set_exception, std::move(prom)) ; try { std::cout << "Waiting for the thread to set the exception...\n" ; fut.get (); } catch (const std::exception& e) { std::cout << "Exception set by the thread: " << e.what () << '\n' ; } t.join (); return 0 ; }
6.5 共享类型的future 当多个线程需要等待同一个执行结果时,可以使用std::shared_future
,也就是多个线程去等待同一个执行结果,不会出现资源竞争的问题,也不用考虑是否加锁(天生线程安全的)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void myFunction (std::promise<int >&& promise) { std::this_thread::sleep_for (std::chrono::seconds (1 )); promise.set_value (42 ); } void threadFunction (std::shared_future<int > future) { try { int result = future.get (); std::cout << "Result: " << result << std::endl; } catch (const std::future_error& e) { std::cout << "Future error: " << e.what () << std::endl; } } void use_shared_future () { std::promise<int > promise; std::shared_future<int > future = promise.get_future (); std::thread myThread1 (myFunction, std::move(promise)) ; std::thread myThread2 (threadFunction, future) ; std::thread myThread3 (threadFunction, future) ; myThread1.join (); myThread2.join (); myThread3.join (); }
补:std::shared_future是支持拷贝的,std::future不支持拷贝,只能移动构造
7. 并发设计模式Actor和CSP 7.1 简介 在并发设计中有两种常用的设计模式,即Actor和CSP模式。传统的并发设计经常都是通过共享内存加锁保证逻辑安全,这种模式有几个缺点:如频繁加锁影响性能、 耦合度高等。而Actor和CSP设计模式恰好可以解决这些缺陷。
7.2 Actor设计模式 actor通过消息传递的方式与外界通信。消息传递是异步的。每个Actor都有一个邮箱,用于接收其他Actor发送的消息,Actor可以按顺序处理邮箱中的消息。actor一次只能同步处理一个消息,处理消息过程中,除了可以接收消息,不能做任何其他操作。 每一个类独立在一个线程里称作Actor,Actor之间通过队列通信,比如Actor1 发消息给Actor2, Actor2 发消息给Actor1都是投递到对方的队列中。好像给对方发邮件,对方从邮箱中取出一样。
Actor模型的另一个好处就是可以消除共享状态,因为它每次只能处理一条消息,所以actor内部可以安全的处理状态,而不用考虑锁机制。一些网络编程中对于逻辑层的处理就采用了该模式,就是将要处理的逻辑消息封装成包投递给逻辑队列,逻辑类通过单线程的方式从队列中一条一条取出数据消费的思想。
7.3 CSP模式 CSP称为通信顺序进程,是一种并发编程模型,也是一个很强大的并发数据模型,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。相对于Actor模型,CSP中channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的channel。
CSP和Actor类似,只不过CSP将消息投递给channel,不关注谁从channel中取数据和发送的一方是谁。而Actor在发送消息前是知道接收方是谁,接受方收到消息后也知道发送方是谁,更像是邮件的通信模式。而csp是完全解耦合的。
但它们有一个共同的特性:不要通过共享内存来通信;相反,通过通信来共享内存 。
7.4 CSP模式的实现 因为go是天然支持csp模式的语言,所以实现起来简单。但c++是一种比较全能的的语言,也可以实现出一种类似于go的channel。下面实现的是类似于生产者和消费者问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 template <typename T> class Channel {private : std::queue<T> queue_; std::mutex mtx_; std::condition_variable cv_producer_; std::condition_variable cv_consumer_; size_t capacity_; bool closed_ = false ; public : Channel (size_t capacity = 0 ) : capacity_ (capacity) {} bool send (T value) { std::unique_lock<std::mutex> lock (mtx_) ; cv_producer_.wait (lock, [this ]() { return (capacity_ == 0 && queue_.empty ()) || queue_.size () < capacity_ || closed_; }); if (closed_) { return false ; } queue_.push (value); cv_consumer_.notify_one (); return true ; } bool receive (T& value) { std::unique_lock<std::mutex> lock (mtx_) ; cv_consumer_.wait (lock, [this ]() { return !queue_.empty () || closed_; }); if (closed_ && queue_.empty ()) { return false ; } value = queue_.front (); queue_.pop (); cv_producer_.notify_one (); return true ; } void close () { std::unique_lock<std::mutex> lock (mtx_) ; closed_ = true ; cv_producer_.notify_all (); cv_consumer_.notify_all (); } }; int main () { Channel<int > ch (10 ) ; std::thread producer ([&]() { for (int i = 0 ; i < 5 ; ++i) { ch.send(i); std::cout << "Sent: " << i << std::endl; } ch.close(); }) ; std::thread consumer ([&]() { std::this_thread::sleep_for(std::chrono::milliseconds(500 )); int val; while (ch.receive(val)) { std::cout << "Received: " << val << std::endl; } }) ; producer.join (); consumer.join (); return 0 ; }
8. 原子操作和内存模型 8.1 改动序列 在一个C++程序中,每个对象都具有一个改动序列,它由所有线程在对象上的全部写操作构成,其中第一个写操作即为对象的初始化。 大部分情况下,这个序列会随程序的多次运行而发生变化,但是在程序的任意一次运行过程中,所含的全部线程都必须形成相同的改动序列(要么都从头开始修改,要么都从尾开始修改)。
改动序列基本要求如下:
只要某线程看到过某个对象,则该线程的后续读操作必须获得相对最近的值,并且,该线程就同一对象的后续写操作,必然出现在改动序列后方(基于上一次的改动)。
如果某线程先向一个对象写数据,过后再读取它,那么必须读取前面写的值。
若在改动序列中,上述读写操作之间还有别的写操作,则必须读取最后写的值。
在程序内部,对于同一个对象,全部线程都必须就其形成相同的改动序列,并且在所有对象上都要求如此。
多个对象上的改动序列只是相对关系,线程之间不必达成一致。
8.2 原子类型 标准原子类型的定义位于头文件<atomic>
内。可以通过atomic<>
定义一些原子类型的变量,如atomic<bool>
,atomic<int>
这些类型的操作全是原子化的。
从C++17开始,所有的原子类型都包含一个静态常量表达式成员变量std::atomic::is_always_lock_free
,它能够返回一个结构在任意给定的目标硬件上是否支持不加锁(无锁结构形式实现)。如果在所有支持该程序运行的硬件上,原子类型X都以无锁结构形式实现,那么这个成员变量的值就为true;否则为false。
std::atomic_flag
是唯一一个不提供is_lock_free()
成员函数的原子类型。因为 std::atomic_flag
本身就是设计为一个简单的、无锁的原子操作。它的操作(设置和清除)总是无锁的,因此不需要 is_lock_free()
函数来查询是否可以无锁执行。
类型std::atomic_flag
的对象在初始化时清零,随后即可通过成员函数test_and_set()
查值并设置成立,或者由clear()
清零。整个过程只有这两个操作。其他的atomic<>
的原子类型都可以基于其实现。
总的来说,std::atomic_flag
的test_and_set
成员函数是一个原子操作,他会先检查std::atomic_flag
当前的状态是否被设置过。如果没有被设置过(比如初始状态或者清除后),将std::atomic_flag
当前的状态设置为true
,并返回false
;如果被设置过则直接返回true
。
对于std::atomic<T>
类型的原子变量,还支持load()
和store()
、exchange()
、compare_exchange_weak()
和compare_exchange_strong()
等操作。
8.3 内存次序 对于原子类型上的每一种操作,我们都可以提供额外的参数,从枚举类std::memory_order
中取值,用于设定所需的内存次序语义。枚举类std::memory_order
具有6个可能的值,包括std::memory_order_relaxed
、std:: memory_order_acquire
、std::memory_order_consume
、std::memory_order_acq_rel
、std::memory_order_release
和 std::memory_order_seq_cst
。
存储(store
)操作,可选用的内存次序有:
std::memory_order_relaxed
、std::memory_order_release
、std::memory_order_seq_cst
载入(load
)操作,可选用的内存次序有:
std::memory_order_relaxed
、std::memory_order_consume
、std::memory_order_acquire
、std::memory_order_seq_cst
读-改-写(read-modify-write
)操作,可选用的内存次序有:
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
原子操作默认使用的是std::memory_order_seq_cst
次序。这六种内存顺序相互组合可以实现三种顺序模型:
Sequencial consistent ordering
:实现同步, 且保证全局顺序一致的模型,是一致性最强的模型, 也是默认的顺序模型
Acquire-release ordering
:实现同步, 但不保证保证全局顺序一致的模型
Relaxed ordering
:不能实现同步, 只保证原子性的模型
8.4 自旋锁的实现 自旋锁是一种在多线程环境下保护共享资源的同步机制。它的基本思想是,当一个线程尝试获取锁时,如果锁已经被其他线程持有,那么该线程就会不断地循环检查锁的状态,直到成功获取到锁为止。
下面程序是使用std:atomic_flag
实现的一个自旋锁,flag初始化为0,当线程1先获得锁后,它会将flag的值设为1,并返回false,这样就不会进入循环,表示加锁成功。当线程2准备进行加锁时,发现flag的值已经被设置为1,所以就返回true,进入循环,不断的检查flag的值来判断。只有当线程1执行完任务后,解锁将flag的值重新设为0时,线程2检查到flag的值为0后,将其设为1,并返回false,才退出循环,表示它加锁成功。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class SpinLock {public : void lock () { while (flag.test_and_set (std::memory_order_acquire)); } void unlock () { flag.clear (std::memory_order_release); } private : std::atomic_flag flag = ATOMIC_FLAG_INIT; }; void TestSpinLock () { SpinLock spinlock; std::thread t1 ([&spinlock]() { spinlock.lock(); for (int i = 0 ; i < 3 ; i++) { std::cout << "*" ; } std::cout << std::endl; spinlock.unlock(); }) ; std::thread t2 ([&spinlock]() { spinlock.lock(); for (int i = 0 ; i < 3 ; i++) { std::cout << "?" ; } std::cout << std::endl; spinlock.unlock(); }) ; t1.join (); t2.join (); }
8.5 宽松内存序(memory_order_relaxed) 下面是CPU的一个内存结构图:
其中StoreBuffer
就是一级Cache, Catche
是二级Cache,Memory
是三级Cache。每个CPU都有一个专属的StoreBuffer
,其对StoreBuffer
的操作对其它的CPU是不可见的。
每个标识CPU的块就是core,上图画的就是4核结构。每两个core构成一个bank,共享一个cache。四个core共享memory。
每个CPU会在任何时刻将StoreBuffer
中结果写入到cache或者memory中。如果CPU1往Catche里面写数据,CPU2也往Catche里面写数据,并且写的是同一个变量,数据就乱了。所以就得通过MESI
一致性协议来保证数据一致性。
MESI
协议,是一种叫作写失效的协议。在写失效协议里,只有一个 CPU 核心负责写入数据,其他的核心,只是同步读取到这个写入。在这个 CPU 核心写入 cache 之后,它会去广播一个“失效”请求告诉所有其他的 CPU 核心。
MESI 协议对应的四个不同的标记,分别是:
M:代表已修。用来告诉其他CPU已经修改完成,其他CPU可以向cache中写入数据
E:代表独占。表示数据只是从Catche加载到当前CPU核的storebuffer中,其他的CPU核,并没有加载对应的数据到自己的storebuffer里。这个时候,如果要向独占的storebuffer写入数据,我们可以自由地写入数据,而不需要告知其他CPU核。
S:代表共享。就是在多核中同时加载了同一份数据到自己的StoreBuffer。所以在共享状态下想要修改数据要先向所有的其他CPU核心广播一个请求,要求先把其他CPU核心里面的cache,都变成无效的状态,然后再更新当前cache里面的数据。
I:代表已失效。如果变量a此刻在各个cpu的StoreBuffer中,那么CPU1核修改这个a的值,放入cache时通知其他CPU核写失效,因为同一时刻仅有一个CPU核可以写数据,但是其他CPU核是可以读数据的,那么其他核读到的数据可能是CPU1核修改之前的。这就涉及我们提到的改动序列了。
关于两个改动序列的术语:
“synchronizes-with
“ : 同步, “A synchronizes-with
B” 的意思就是 A和B同步,简单来说如果多线程环境下,有一个线程先修改了变量m,我们将这个操作叫做A,之后有另一个线程读取变量m,我们将这个操作叫做B,那么B一定读取A修改m之后的最新值。也可以称作 A “happens-before
“ B,即A操作的结果对B操作可见。
“happens-before
“ : 先行,”A happens-before
B” 的意思是如果A操作先于B操作,那么A操作的结果对B操作可见。”happens-before
“包含很多种境况,不仅仅是上面提到的”synchronizes-with
“。
关于std::memory_order_relaxed
具备如下几个功能:
作用于原子变量
不具有synchronizes-with
关系,即如果线程1对m进行写,由0变成1,线程2读到的很有可能是0
对于同一个原子变量,在同一个线程中具有happens-before
关系;在同一线程中不同的原子变量不具有happens-before
关系,可以乱序执行
多线程情况下不具有happens-before
关系
在下面程序中,write_x_then_y()
是先写x再写y,都写为true;read_y_then_x()
是先读y,再读x,最后通过TestOrderRelaxed()
来调用实现,看是否会出现断言。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 std::atomic<bool > x, y; std::atomic<int > z; void write_x_then_y () { x.store (true , std::memory_order_relaxed); y.store (true , std::memory_order_relaxed); } void read_y_then_x () { while (!y.load (std::memory_order_relaxed)) { std::cout << "y load false" << std::endl; } if (x.load (std::memory_order_relaxed)) { ++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 ); }
从CPU架构分析:假设线程t1运行在CPU1上,t2运行在CPU3上,那么t1对x和y的操作,t2是看不到的。比如当线程t1运行至1处将x设置为true,t1运行至2处将y设置为true。这些操作仅在CPU1的storebuffer中,还未放入cache和memory中,CPU2和CPU3自然不知道。如果CPU1先将y放入memory,那么CPU3就会读取y的值为true。那么t2就会运行至3处从while循环退出,进而运行至4处,此时CPU1还未将x的值写入memory,t2读取的x值为false,进而线程t2运行结束,然后CPU1将x写入true,t1结束运行,最后主线程运行至5处,因为z为0,所以触发断言。
从宽松内存序分析:因为memory_order_relaxed
是宽松的内存序列,它只保证操作的原子性,并不能保证多个变量之间的顺序性,也不能保证同一个变量在不同线程之间的可见顺序。可以理解为不能保证可见的及时性,但如果有修改,迟早是可见的。
在下面程序中,线程1和线程2往里面去写值,线程3和线程4往a里面去读值分别存到v3和v4中。需要注意的是,虽然线程1和线程2是抢着去修改a,但它们的操作一定是满足原子性的,即两个线程去修改,只有其中一个线程修改完,另一个线程才会去修改。而读也能保证一个原子性,即线程3和线程4读出来的数据a,一定是某一时刻线程1或线程2对a修改的一个值,但读到的可能不是最新的,所以就可能是一堆乱序的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void TestOderRelaxed2 () { std::atomic<int > a{0 }; std::vector<int > v3, v4; std::thread t1 ([&a]() { for (int i = 0 ; i < 10 ; i += 2 ) { a.store(i, std::memory_order_relaxed); } }) ; std::thread t2 ([&a]() { for (int i = 1 ; i < 10 ; i += 2 ) a.store(i, std::memory_order_relaxed); }) ; std::thread t3 ([&v3, &a]() { for (int i = 0 ; i < 10 ; ++i) v3.push_back(a.load(std::memory_order_relaxed)); }) ; std::thread t4 ([&v4, &a]() { for (int i = 0 ; i < 10 ; ++i) v4.push_back(a.load(std::memory_order_relaxed)); }) ; t1.join (); t2.join (); t3.join (); t4.join (); for (int i : v3) { std::cout << i << " " ; } std::cout << std::endl; for (int i : v4) { std::cout << i << " " ; } std::cout << std::endl; }
在上面程序,如果v3中7先于6,8,9等,那么v4中也是7先于6,8,9。总的来说,memory_order_relaxed
保证了多线程对同一个变量的原子操作的安全性,只是可见性会有延迟。
8.6 先行(Happens-before) Happens-before
是一个非常重要的概念。如果操作 a “happens-before
” 操作 b,则操作 a 的结果对于操作 b 可见。happens-before
的关系可以建立在用一个线程的两个操作之间,也可以建立在不同的线程的两个操作之间。
顺序先行(sequenced-before):单线程情况下前面的语句先执行,后面的语句后执行。操作a先于操作b,那么操作b可以看到操作a的结果。我们称操作a顺序先行于操作b。也就是”a sequenced-before b”。
线程间先行:
依赖关系:单线程情况下a “sequenced-before” b, 且 b 依赖 a 的数据, 则 a “carries a dependency into” b. 称作 a 将依赖关系带给 b, 也理解为b依赖于a。