C++11 多线程并发 互斥量、条件变量
互斥量
C++11提供4种互斥量语义:
(C++11)
|
provides basic mutual exclusion facility (class) |
(C++11)
|
provides mutual exclusion facility which implements locking with a timeout (class) |
(C++11)
|
provides mutual exclusion facility which can be locked recursively by the same thread (class) |
(C++11)
|
provides mutual exclusion facility which can be locked recursively by the same thread and implements locking with a timeout (class) |
- std::mutex 独占互斥量,不能递归加锁;
- std::timed_mutex 带超时的独占互斥量,超时自动解锁,不能递归加锁;
- std::recursive_mutex 递归互斥量,不带超时解锁功能;
- std::recursive_timed_mutex 带超时功能的递归互斥量,超时自动解锁,能递归加锁;
独占互斥量std::mutex
mutex 类是一个同步原语,可用于保护共享数据不被多个线程同时访问。
互斥量提供独占的、非递归的所有权语义:
调用线程从成功调用lock或try_lock直到调用unlock为止都拥有互斥体。
1.当一个线程拥有互斥锁时,如果所有其他线程尝试声明该互斥锁的所有权,则所有其他线程都将阻塞(对于 lock 调用)或收到错误的返回值(对于 try_lock)。
2.在调用 lock 或 try_lock 之前,调用线程不得拥有互斥量。
如果互斥锁在仍由任何线程拥有时被销毁,或者线程在拥有互斥锁时终止,则程序的行为是未定义的。 mutex 类满足 Mutex 和 StandardLayoutType 的所有要求。
std::mutex 既不可复制也不可移动。
加锁操作:
lock() 加锁,独占性,锁定互斥量,如果互斥量不可用则阻塞。
trylock() 尝试锁定互斥量,如果互斥量不可用则返回
unlock() 对互斥量解锁
示例代码:
#include <chrono> #include<iostream> #include<thread> #include<mutex> using namespace std; std::mutex mutex_; void func(){ mutex_.lock(); cout << "enter thread [" << this_thread::get_id() << "]" << endl; this_thread::sleep_for(chrono::seconds(1)); cout << "leaving thread [" << this_thread::get_id() << "]" << endl; mutex_.unlock(); } int main(int argc, char *argv[]){ thread t1(func); thread t2(func); thread t3(func); t1.join(); t2.join(); t3.join(); return 0; }
运行结果:
enter thread [139774937843456] leaving thread [139774937843456] enter thread [139774929450752] leaving thread [139774929450752] enter thread [139774921058048] leaving thread [139774921058048]
std::lock_guard
lock_guard 类是一个互斥锁包装器,它提供了一种方便的 RAII 样式机制,用于在作用域块的持续时间内拥有互斥锁。
当创建 lock_guard 对象时,它会尝试获取所给定互斥锁的所有权。当控制权离开创建 lock_guard 对象的范围时,lock_guard 将被破坏并释放互斥锁。
lock_guard 类是不可复制的。
注意:初学者常见的一个错误是“忘记”给 lock_guard 变量命名,例如std::lock_guard(mtx); (默认构造一个名为 mtx 的 lock_guard 变量)或 std::lock_guard{mtx}; (它构造了一个立即销毁的纯右值对象),因此实际上并没有构造一个为其余作用域保存互斥锁的锁。
std::scoped_lock 提供了 lock_guard 的替代方案,它提供了使用死锁避免算法锁定多个互斥体的能力。(C++17)
void func(){ std::lock_guard<mutex> lck(mutex_); // mutex_.lock(); cout << "enter thread [" << this_thread::get_id() << "]" << endl; this_thread::sleep_for(chrono::seconds(1)); cout << "leaving thread [" << this_thread::get_id() << "]" << endl; // mutex_.unlock(); }
递归互斥量std::recursive_mutex
递归互斥量又称递归锁,可以解决同一个线程多次获取同一个互斥量导致死锁问题。不过,要求解锁次数 等于 加锁次数,否则不能正常解锁。
recursive_mutex 类是一个同步原语,可用于保护共享数据不被多个线程同时访问。
recursive_mutex 提供独占的、递归的所有权语义:
调用线程在成功调用 lock 或 try_lock 时开始一段时间内拥有 recursive_mutex。在此期间,线程可能会额外调用lock或try_lock。当线程发出匹配数量的解锁调用时,所有权期限结束。
当一个线程拥有 recursive_mutex 时,如果所有其他线程尝试声明 recursive_mutex 的所有权,则它们将阻塞(对于调用 lock)或收到错误的返回值(对于 try_lock)。
recursive_mutex 可以被锁定的最大次数是未指定的,但在达到该次数后,对 lock 的调用将抛出 std::system_error 并且对 try_lock 的调用将返回 false。
如果 recursive_mutex 被销毁但仍由某个线程拥有,则程序的行为是未定义的。 recursive_mutex 类满足 Mutex 和 StandardLayoutType 的所有要求。
#include <chrono> #include<iostream> #include<thread> #include<mutex> using namespace std; struct Complex{ // std::mutex mutex_; std::recursive_mutex mutex_; int val_; Complex() : val_(0){} void mul(int x){ // std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::recursive_mutex> lock(mutex_); val_ *= x; } void div(int x){ // std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::recursive_mutex> lock(mutex_); val_ /= x; } void both(int x, int y){ // std::lock_guard<std:: mutex> lock(mutex_); std::lock_guard<std::recursive_mutex> lock(mutex_); mul(x); div(y); } }; int main(int argv, char *argc[]){ Complex complex; complex.both(32, 23); return 0; }
TIP:能不使用递归锁,尽量不用。原因在于:
1)需要用到递归锁的多线程互斥处理的情况,本身往往可以简化,而允许递归互斥很容易导致复杂逻辑的产生,从而导致多线程同步引起的晦涩难懂的问题;
2)递归锁比非递归锁,效率更低;
3)递归锁虽然允许同一线程多次获得同一个互斥量,可重复获得的最大次数并未具体说明,但一旦超过一定次数,再调用lock会抛出std::system错误。
带超时的互斥量std::timed_mutex及std::recursive_timed_mutex
timed_mutex是超时的独占锁,在mutex基础上增加了超时等待功能。
timed_mutex 类是一个同步原语,可用于保护共享数据不被多个线程同时访问。
与互斥量类似,timed_mutex 提供独占的、非递归的所有权语义。此外,timed_mutex 还提供了通过成员函数 try_lock_for() 和 try_lock_until() 尝试在超时时声明 timed_mutex 所有权的能力。
timed_mutex 类满足 TimedMutex 和 StandardLayoutType 的所有要求。
recursive_timed_mutex是超时递归锁,在recursive_mutex基础上增加了超时等待功能。
recursive_timed_mutex 类是一个同步原语,可用于保护共享数据不被多个线程同时访问。
以类似于 std::recursive_mutex 的方式,recursive_timed_mutex 提供独占的、递归的所有权语义。此外,recursive_timed_mutex 还提供了通过 try_lock_for 和 try_lock_until 成员函数尝试声明超时的 recursive_timed_mutex 所有权的能力。
recursive_timed_mutex 类满足 TimedMutex 和 StandardLayoutType 的所有要求。
超时等待功能是指,等待指定时间后,如果还未取得锁,不再阻塞。
timed_mutex mutex_; void work() { chrono::microseconds timeout(100); // 100 ms while (true) { // try to wait the lock if (mutex_.try_lock_for(timeout)) { // success to get the lock cout << this_thread::get_id() << ": do work with the mutex" << endl; chrono::milliseconds sleepDuration(250); this_thread::sleep_for(sleepDuration); mutex_.unlock(); } else { // timed out, fail to get the lock cout << this_thread::get_id() << ": do work without the mutex" << endl; chrono::milliseconds sleepDuration(100); this_thread::sleep_for(sleepDuration); } } } int main(int argc, char *argv[]) { thread t1(work); thread t2(work); t1.join(); t2.join(); return 0; }
条件变量
条件变量,是一种用于多线程等待的同步机制。条件变量能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时,才会唤醒当前阻塞的线程。
条件变量需要和互斥量搭配使用。
C++11提供2种条件变量:
- condition_variable 搭配
std::unique_lock<std::mutex>
进行wait操作; - condition_variable_any 搭配任意带有lock/unlock语义的mutex使用,较灵活,但效率比condition_variable 更低;
头文件:<condition_variable>
std::condition_variable
condition_variable 类是与 std::mutex 一起使用的同步原语,用于阻止一个或多个线程,直到另一个线程修改共享变量(条件)并通知 condition_variable。
打算修改共享变量的线程必须:
1.获取 std::mutex(通常通过 std::lock_guard)。
2.在拥有锁的情况下修改共享变量。
3.对 std::condition_variable 调用notify_one或notify_all(可以在释放锁后完成)。
即使共享变量是原子的,也必须在拥有互斥体的同时对其进行修改,才能正确地将修改发布到等待线程。
任何想要等待 std::condition_variable 的线程必须:
1.获取用于保护共享变量的互斥锁上的 std::unique_lock<std::mutex>。
2.执行以下操作之一:
2.1检查情况,是否已更新并通知。
2.2对 std::condition_variable 调用 wait、wait_for 或 wait_until(以原子方式释放互斥体并挂起线程执行,直到通知条件变量、超时到期或发生虚假唤醒,然后在返回之前以原子方式获取互斥体)。
2.3检查条件,如果不满足则继续等待。
或者:
使用 wait、wait_for 和 wait_until 的谓词重载,它执行相同的三个步骤。
std::condition_variable 仅适用于 std::unique_lock<std::mutex>,它允许在某些平台上实现最大效率。 std::condition_variable_any 提供可与任何 BasicLockable 对象一起使用的条件变量,例如 std::shared_lock。
条件变量允许同时调用 wait、wait_for、wait_until、notify_one 和 notify_all 成员函数。
类 std::condition_variable 是一个 StandardLayoutType。它不是可复制构造、可移动构造、可复制分配或可移动分配。
std::condition_variable 仅适用于 std::unique_lock<std::mutex>
示例
#include<condition_variable> #include<iostream> #include<mutex> #include<string> #include<thread> using namespace std; std::mutex m; std::condition_variable cv; string data; bool re = false; bool pro = false; void worker_thread(){ std::unique_lock<std::mutex> lock(m); cv.wait(lock, []{return re;}); cout << "Worker thread is processing data\n"; data += " after processing"; pro = true; cout << "Worker thread signals data processing completed\n"; lock.unlock(); cv.notify_one(); } int main() { std::thread worker(worker_thread); data = "Data"; { std::lock_guard<std::mutex> lock(m); re = true; cout << "main() signals data ready for processing\n"; } cv.notify_one(); { std::unique_lock<std::mutex> lock(m); cv.wait(lock, []{return pro;}); } cout << "back in main(), data = " << data << '\n'; worker.join(); }
运行结果:
main() signals data ready for processing Worker thread is processing data Worker thread signals data processing completed back in main(), data = Data after processing
std::condition_variable_any
condition_variable_any 类是 std::condition_variable 的泛化。 std::condition_variable 仅适用于 std::unique_lock<std::mutex>,而 condition_variable_any 可以对满足 BasicLockable 要求的任何锁进行操作。
有关条件变量语义的描述,请参见 std::condition_variable。
类 std::condition_variable_any 是一个 StandardLayoutType。它不是可复制构造、可移动构造、可复制分配或可移动分配。
如果锁是 std::unique_lock<std::mutex>,则 std::condition_variable 可能会提供更好的性能。
注意:
std::condition_variable_any 可以与 std::shared_lock 一起使用,以便在共享所有权模式下等待 std::shared_mutex。
std::condition_variable_any 与自定义 Lockable 类型的可能用途是提供方便的可中断等待:自定义锁定操作将按预期锁定关联的互斥锁,并在收到中断信号时执行必要的设置以通知此条件变量。
示例代码:
#include<condition_variable> #include <cstddef> #include<iostream> #include<mutex> #include<string> #include<thread> #include<list> using namespace std; template<typename T> class SyncQueue{ private: std::list<T> queue_; std::mutex mutex_; std::condition_variable_any not_empty_; std::condition_variable_any not_full_; int max_size_; bool IsFull() const { return queue_.size() == max_size_; } bool isEmpty() const { return queue_.empty(); } public: SyncQueue(int max_size) : max_size_(max_size) { } void Put(const T& x){ std::lock_guard<std::mutex> lock(mutex_); not_full_.wait(mutex_, [this]{return !this->IsFull();}); queue_.push_back(x); not_empty_.notify_one(); } void Take(T& x){ std::lock_guard<std::mutex> lock(mutex_); not_empty_.wait(mutex_, [this]{return !this->isEmpty();}); x = queue_.front(); queue_.pop_front(); not_full_.notify_one(); } bool Empty() { std::lock_guard<std::mutex> lock(mutex_); return queue_.empty(); } bool Full(){ std::lock_guard<std::mutex> lock(mutex_); return queue_.size() == max_size_; } size_t Size(){ std::lock_guard<std::mutex> lock(mutex_); return queue_.size(); } }; SyncQueue<int> myQue(5); void Producer(){ for(int i = 0; i < 10; ++i){ myQue.Put(i); cout << "Produced: " << i << endl; } } void Consumer(){ for(int i = 0; i < 10; ++i){ myQue.Take(i); cout << "Consumed: " << i << endl; } } int main(){ std::thread P(Producer); std::thread C(Consumer); P.join(); C.join(); return 0; }
运行结果:
Produced: 0 Produced: 1 Produced: 2 Produced: 3 Produced: 4 Consumed: 0 Consumed: 1 Consumed: 2 Consumed: 3 Consumed: 4 Produced: 5 Produced: 6 Produced: 7 Produced: 8 Produced: 9 Consumed: 5 Consumed: 6 Consumed: 7 Consumed: 8 Consumed: 9
call_once/once_flag
Linux有pthread_once可以确保函数只被调用一次,C++有没有类似技术?
答案是有的,那就是使用call_once/once_flag。在多线程环境中,如需要某个对象只初始化一次,可以用std::call_once。用std::call_once时,需要提供一个once_flag参数。
示例代码:
#include<condition_variable> #include<iostream> #include<mutex> #include<string> #include<thread> using namespace std; std::once_flag g_flag; void work() { call_once(g_flag, []() { cout << "Called once" << endl; }); } int main(int argc, char *argv[]) { thread t1(work); thread t2(work); thread t3(work); thread t4(work);
... t1.join(); t2.join(); t3.join(); t4.join();
... return 0; }
运行结果:
Called once
不管开多少个线程,只会调用一次函数。