C++11 多线程之互斥量、条件变量使用简介
2022/1/17 1:03:55
本文主要是介绍C++11 多线程之互斥量、条件变量使用简介,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
目录- 互斥量
- 独占互斥量std::mutex
- 递归互斥量 std::recursive_mutex
- 带超时的互斥量std::timed_mutex及std::recursive_timed_mutex
- 条件变量
- condition_variable
- unique_lock与lock_guard
互斥量
C++11提供4种互斥量(mutex)语义,对于4个类:
- std::mutex 独占互斥量,不能递归加锁;
- std::timed_mutex 带超时的独占互斥量,超时自动解锁,不能递归加锁;
- std::recursive_mutex 递归互斥量,不带超时解锁功能;
- std::recursive_timed_mutex 带超时功能的递归互斥量,超时自动解锁,能递归加锁;
头文件:
独占互斥量std::mutex
独占互斥量又称互斥量,互斥锁,独占锁,顾名思义,同一时刻只能有一个线程取得该锁,其他试图取得该锁的线程阻塞,待持有锁的线程释放独占锁时,才能唤醒取得独占锁后继续运行。
互斥量不允copy操作(copy构造、copy assignment),不允许move操作(move构造、move assignment),最初参数的mutex对象是unlocked(未加锁)状态。
mutex的同样操作
1)lock(),加锁,独占性占用互斥量资源;
2)unlock(),解锁,解除对互斥量的占用,必须和lock成对出现;
3)try_lock(),尝试锁定互斥量,成功返回true;失败返回false,非阻塞;
示例:
#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)); // 休眠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 [13660] leaving thread [13660] enter thread [13372] leaving thread [13372] enter thread [13368] leaving thread [13368]
lock_guard与mutex
lock_guard类可以简化mutex的lock/unlock写法,利用loak_guard对象的构造对mutex加锁,对象的析构对mutex解锁。即所谓RAII技术。这样,可以保证在资源除了作用域后就释放,即使中间发生异常,也能正常解锁。缺点是,会带来额外的对象构造和析构性能消耗。
将上面的例子,改造成利用lock_guard lock/unlock:
void func() { lock_guard<mutex> lck(mutex_); // 自动对mutex_加锁(lock) cout << "enter thread [" << this_thread::get_id() << "]" << endl; this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒 cout << "leaving thread [" << this_thread::get_id() << "]" << endl; // 退出函数作用域时,析构loak_guard对象,自动释放mutex_锁(unlock) }
递归互斥量 std::recursive_mutex
递归互斥量又称递归锁,可以解决同一个线程多次获取同一个互斥量导致死锁问题。不过,要求解锁次数 等于 加锁次数,否则不能正常解锁。
示例:
// 同一线程多次获取同一个互斥量导致死锁问题的例子 struct Complex { std::mutex mutex_; int val_; Complex() : val_(0) {} void mul(int x) { std::lock_guard<std::mutex> lock(mutex_); val_ *= x; } void div(int x) { std::lock_guard<std::mutex> lock(mutex_); val_ /= x; } void both(int x, int y) { std::lock_guard<std::mutex> lock(mutex_); mul(x); // 同一线程多次对mutex_加锁,会导致死锁 div(y); // 同一线程多次对mutex_加锁,会导致死锁 } }; int main(int argc, char *argv[]) { Complex complex; complex.both(32, 23); return 0; }
将例子改造成使用递归锁recursive_mutex
// 使用递归锁recursive_mutex struct Complex { std::recursive_mutex mutex_; int val_; Complex() : val_(0) {} void mul(int x) { std::lock_guard<std::recursive_mutex> lock(mutex_); val_ *= x; } void div(int x) { std::lock_guard<std::recursive_mutex> lock(mutex_); val_ /= x; } void both(int x, int y) { std::lock_guard<std::recursive_mutex> lock(mutex_); mul(x); // 不会产生死锁 div(y); // 不会产生死锁 } }; ...
TIP:能不使用递归锁,尽量不用。原因在于:
1)需要用到递归锁的多线程互斥处理的情况,本身往往可以简化,而允许递归互斥很容易导致复杂逻辑的产生,从而导致多线程同步引起的晦涩难懂的问题;
2)递归锁比非递归锁,效率更低;
3)递归锁虽然允许同一线程多次获得同一个互斥量,可重复获得的最大次数并未具体说明,但一旦超过一定次数,再调用lock会抛出std::system错误。
带超时的互斥量std::timed_mutex及std::recursive_timed_mutex
timed_mutex是超时的独占锁,在mutex基础上增加了超时等待功能。
recursive_timed_mutex是超时递归锁,在recursive_mutex基础上增加了超时等待功能。
超时等待功能是指,等待指定时间后,如果还未取得锁,不再阻塞。
timed_mutex 示例
recursive_timed_mutex类似
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>
condition_variable
condition_variable 的5个函数:
- wait 阻塞当前线程,等待唤醒;
- wait_for 阻塞当前线程,等待唤醒,最多等待一段时间;
- wait_until 阻塞当前线程,等待唤醒,最多等待到某个时间点;
- notify_one 唤醒一个等待在这个条件变量上的线程;
- notify_all 唤醒所有等待在这个条件变量上的线程;
condition_variable_any 也拥有这5个函数。
使用condition_variable_any搭配lock_guard示例
同步队列:当队列满时,阻塞插入线程,无法再往内部插入数据,直到另一个线程取走数据满足队列非满条件;
当队列空时,阻塞取数据线程,无法再从内部取走数据,直到另一个线程插入数据满足队列非空条件。
/** 同步队列类 */ template<typename T> class SyncQueue { private: // 内部使用, 非线程安全 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> lck(mutex_); /* while语句 <=> not_full_.wait(mutex_, [this] { return !this->IsFull(); }) */ while (IsFull()) { cout << "缓冲区满了,需要等待..." << endl; not_full_.wait(mutex_); // 等待条件not_full_ } queue_.push_back(x); not_empty_.notify_one(); // 随机唤醒一个等待在条件变量not_empty_上的线程 } // 取出数据 void Take(T& x) { std::lock_guard<std::mutex> lck(mutex_); while (IsEmpty()) { cout << "缓冲区空了,需要等待..." << endl; not_empty_.wait(mutex_); // 等待条件not_empty_ } x = queue_.front(); queue_.pop_front(); not_full_.notify_one(); // 随机唤醒一个等待在条件变量not_full_上的线程 } // 公共接口,线程安全,注意mutex_是独占锁(下面3个函数同) bool Empty() { std::lock_guard<std::mutex> lck(mutex_); return queue_.empty(); } bool Full() { std::lock_guard<std::mutex> lck(mutex_); return queue_.size() == max_size_; } size_t Size() { std::lock_guard<std::mutex> lck(mutex_); return queue_.size(); } private: std::list<T> queue_; std::mutex mutex_; std::condition_variable_any not_empty_; std::condition_variable_any not_full_; int max_size_; };
注意:Put中的wait代码可以改写成lambda形式
std::lock_guard<std::mutex> lck(mutex_); while (IsFull()) { cout << "缓冲区满了,需要等待..." << endl; not_full_.wait(mutex_); // 等待条件not_full_ } // 可以改写成 not_full_.wait(mutex_, [this] { return !this->IsFull(); })
wait的第二个参数判别式为true时,线程不会放弃锁,会继续执行;当判别式为false时,线程放弃锁,阻塞。
unique_lock与lock_guard
由于condition_variable_any 只能搭配unique_lock使用,我们研究下unique_lock与lock_guard有何区别?
最大区别在于,unique_lock不像lock_guard只能在析构时才释放锁,而是可以随时调用unlock释放锁。另外,可以构造一个空的unique_lock,却无法构造一个空的lock_guard,也就是说,lock_guard必须绑定一个mutex。
使用condition_variable搭配unique_lock示例
我们将上面condition_variable_any + lock_guard的同步队列示例,修改为condition_variable + unique_lock
/** 同步队列类 */ template<typename T> class SyncQueue { ... // 插入数据 void Put(const T& x) { std::unique_lock<std::mutex> lck(mutex_); not_full_.wait(mutex_, [this] { return !this->IsFull(); }) queue_.push_back(x); not_empty_.notify_one(); // 随机唤醒一个等待在条件变量not_empty_上的线程 } // 取出数据 void Take(T& x) { std::unique_lock<std::mutex> lck(mutex_); not_full_.wait(mutex_, [this] { return !this->IsEmpty(); }); x = queue_.front(); queue_.pop_front(); not_full_.notify_one(); // 随机唤醒一个等待在条件变量not_full_上的线程 } ... private: ... std::mutex mutex_; std::condition_variable not_empty_; std::condition_variable not_full_; ... };
这篇关于C++11 多线程之互斥量、条件变量使用简介的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23增量更新怎么做?-icode9专业技术文章分享
- 2024-11-23压缩包加密方案有哪些?-icode9专业技术文章分享
- 2024-11-23用shell怎么写一个开机时自动同步远程仓库的代码?-icode9专业技术文章分享
- 2024-11-23webman可以同步自己的仓库吗?-icode9专业技术文章分享
- 2024-11-23在 Webman 中怎么判断是否有某命令进程正在运行?-icode9专业技术文章分享
- 2024-11-23如何重置new Swiper?-icode9专业技术文章分享
- 2024-11-23oss直传有什么好处?-icode9专业技术文章分享
- 2024-11-23如何将oss直传封装成一个组件在其他页面调用时都可以使用?-icode9专业技术文章分享
- 2024-11-23怎么使用laravel 11在代码里获取路由列表?-icode9专业技术文章分享
- 2024-11-22怎么实现ansible playbook 备份代码中命名包含时间戳功能?-icode9专业技术文章分享