前言
多线程因其调度的随机性和时间片分配,如果没有限制的访问临界资源,会导致出现无法预测的结果,也无法达到预期。
所以,访问临界区,需要是原子性的,在一个线程完成之前,不能有其他线程访问,影响。
互斥量的底层原理可以参看[Linux]线程互斥
在C++11的线程库中,有很多适用于不同场景的互斥量
一. mutex
mutex是互斥锁的意思,其成员函数如下:
1. 构造函数
函数声明 | 说明 |
---|---|
constexpr mutex() noexcept | 不会抛异常的无参构造 |
mutex(const mutex&) = delete | 不支持拷贝构造 |
PS:
noexcept
在函数声明后作标识符,默认是noexcept(true),表示不会抛异常
constexpr
:让该函数在编译时生成,而不是运行时
=delete
在函数声明后,表示不会生成该函数
2. 加锁与解锁
C++11线程库其实就是对系统调用的封装,将其封装成一个类
函数声明 | 说明 |
---|---|
void lock() | 加锁 |
bool try_lock() | 尝试加锁 |
void unlock() | 解锁 |
注意,线程函数调用lock()
时,可能会发生以下三种情况:
- 如果该互斥锁当前没有被锁住,则调用线程将获取到互斥锁,直到调用unlock之前,该线程一直拥有该锁
- 如果当前互斥锁被其他线程锁住,则当前调用线程会被阻塞在获取锁的lock()函数处
- 如果当前互斥锁被当前调用线程锁住,则会产生死锁(deadlock)
try_lock()
也分为3种情况
- 没有线程持有锁,则调用try_lock的线程获得锁
- 其他线程持有锁,则加锁失败,返回false
- 当前线程持有锁,不进行操作
二. recursive_mutex
关于死锁的概念,可以参看Linux死锁
如果我们在递归中使用互斥锁,就会出现死锁的情况
mutex _mutex;
void Func(int n)
{
if (n == 0)
{
return;
}
_mutex.lock();
cout<<n<<endl;
Func(n-1);
_mutex.unlock();
}
int main()
{
thread t1(Func, 7);
t1.join();
return 0;
}
为了解决这一问题,C++11提供了递归互斥锁 recursive_mutex
其允许同一个线程对互斥锁多次上锁(即递归上锁),来获得互斥量对象的多层所有权,释放互斥锁时需要调用与该锁层次深度
相同次数的unlock()
。除此之外,基本用法同mutex
三. timed_mutex
timed_mutex是定时互斥锁
多提供了try_lock_for和try_lock_until
- try_lock_for:
接受一个时间范围
,表示在一段时间范围之内线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得互斥锁,如果超时(即在时间范围内没有获取到锁),则返回false - try_lock_until:
接受一个时间点
作为参数,在指定时间点未到来之前线程如果没有获取到锁,则一直阻塞,如果在此期间其他线程释放了锁,则该线程可以获得互斥锁,如果超时(即在时间范围内没有获取到锁),则返回false
四. lock_guard和unique_lock
手动的加锁与解锁难免有些麻烦,于是C++11根据RAII习语管理资源,lock_guard在构造函数中自动绑定构造互斥锁,并且加锁,大大减少了死锁的风险,并且在析构函数中调用解锁,避免了忘记解锁等不必要的麻烦。
1. 构造函数
函数声明 | 说明 |
---|---|
explicit lock_guard(mutex_type&m) | 构造函数,需要传一把锁 |
lock_guard(mutex_type& m,adopt_lock tag) | 将锁转移到lock_guard的锁 |
lock_guard(const lock_guard&) = delete | 不支持拷贝构造 |
adopt_lock的作用正如他的命名,寄养锁,使用如下:
std::mutex _mutex;
void test5() {//std::adopt_mutex的大妙处
_mutex.lock();
lock_guard<std::mutex> lg(_mutex, std::adopt_lock);
//在这里进行收养锁
cout << "hello test5" << endl;
}
这样就将_mutex的锁转移到lock_guard中,由lock_guard管理
但是lock_guard只会在构造时加锁,析构时解锁,如果途中我们有解锁和需求则无法完成,所以unique_lock出现了。
unique_lock同样也遵守RAII习语管理资源,构造时加锁,析构时解锁,但是unique_lock还支持手动加锁和解锁
- 上锁/解锁操作:lock,try_lock,try_lock_for,try_lock_until和unlock
- 修改操作:移动赋值,交换swap(与另一个unique_lock对象互换锁管理的互斥锁所有权),release(返回它所管理的互斥锁对象的指针,并释放所有权)
- 获取属性:owns_lock(返回当前对象是否上锁),operator bool()(与owns_lock功能相同),mutex(返回当前unique_lock所管理的互斥锁的指针)
结束语
感谢你的阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。