C++11 多线程之互斥量、条件变量使用简介
作者:互联网
互斥量
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_;
...
};
标签:11,std,lock,互斥,guard,mutex,线程,多线程 来源: https://www.cnblogs.com/fortunely/p/15811707.html