目录
1. mutex
下面我们来看一下互斥锁. mutex的主要接口是:
- lock: 如果有其他线程抢到锁了, 那么这个线程就处在阻塞状态
- try_lock: 如果有其他人抢到锁了, 那么我这个线程就不阻塞了, 返回 false, 更加灵活一些.
2. timed_mutex
除了 mutex 相关的接口, cpp11 当中还支持一个功能更加丰富的 timed_mutex(定时互斥锁 )
, 下面来看看其相关的接口:
相比于一般的 mutex(互斥锁), 这个锁的话是有一个"定时"的功能的.
- try_lock_for: 加入一个最大的时间限定, for 代表一个时间段, 比如 200ms.
- try_lock_until: 这个的话就是最多锁到哪个时间点. until 代表的是时间点的概念.
下面展示一个 until 的一个具体用法示例:
// timed_mutex::try_lock_until example
#include <iostream> // std::cout
#include <chrono> // std::chrono::system_clock
#include <thread> // std::thread
#include <mutex> // std::timed_mutex
#include <ctime> // std::time_t, std::tm, std::localtime, std::mktime
std::timed_mutex cinderella;
// gets time_point for next midnight:
std::chrono::time_point<std::chrono::system_clock> midnight() {
using std::chrono::system_clock;
std::time_t tt = system_clock::to_time_t (system_clock::now());
struct std::tm * ptm = std::localtime(&tt);
++ptm->tm_mday; ptm->tm_hour=0; ptm->tm_min=0; ptm->tm_sec=0;
return system_clock::from_time_t (mktime(ptm));
}
void carriage() {
if (cinderella.try_lock_until(midnight())) {
std::cout << "ride back home on carriage\n";
cinderella.unlock();
}
else
std::cout << "carriage reverts to pumpkin\n";
}
void ball() {
cinderella.lock();
std::cout << "at the ball...\n";
cinderella.unlock();
}
int main ()
{
std::thread th1 (ball);
std::thread th2 (carriage);
th1.join();
th2.join();
return 0;
}
这个代码啥意思呢? 就是两个线程 th1 和 th2 在互斥的运行打印... th1 执行void ball()
, 然后 th2 执行void carriage()
, 打印的时候都是加了锁了, 这样可以避免打印到显示器上的信息错乱.
不过有意思的是, th2 在执行carriage
的时候, 他最多竞争锁到第二天的零时零分零秒, 此后就不再参与竞争锁了, 之后灰姑娘的马车就变成了南瓜...
carriage
函数模拟了灰姑娘乘坐马车回家的场景。它会尝试获取一个互斥锁,这个锁代表了马车的使用权限。获取锁的尝试会持续到下一个午夜(即第二天的 00:00:00)。如果在这个时间之前成功获取到锁,就意味着灰姑娘可以乘坐马车回家;如果到了午夜还没有获取到锁,那么马车就会变回南瓜。
3. recursive_mutex
还有一个递归锁recursive_mutex
. 因为互斥锁有个小问题, 递归自己可能会吧自己吧自己死锁掉.
这个递归锁呢就是lock()的时候做了一个事情, 第一次锁就直接锁, 如果同一个线程再次过来锁就继续锁, 不会出现死锁情况.
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx;
// 递归函数,尝试多次加锁
void recursiveFunction(int n) {
// 尝试加锁
mtx.lock();
std::cout << "Acquired lock at depth: " << n << std::endl;
if (n > 0) {
// 递归调用自身
recursiveFunction(n - 1);
}
// 尝试解锁
mtx.unlock();
std::cout << "Released lock at depth: " << n << std::endl;
}
int main() {
// 调用递归函数
recursiveFunction(3);
return 0;
}
当第一次调用 recursiveFunction
时,线程成功获取了锁。但在递归调用 recursiveFunction
时,线程再次尝试获取同一个锁,由于 std::mutex
不允许同一个线程多次获取同一把锁,所以线程会被阻塞,等待锁被释放。然而,锁的释放操作需要在递归调用返回后才能执行,这就导致线程自己等待自己释放锁,从而造成死锁。
实际上,同一个线程再次对 std::recursive_mutex
加锁时,依然会成功加锁,并非 “不锁”。std::recursive_mutex
会记录加锁的次数,每次调用 lock()
方法时,加锁次数就会增加;每次调用 unlock()
方法时,加锁次数就会减少。只有当加锁次数降为 0 时,锁才会真正被释放,其他线程才能获取该锁。
#include <iostream>
#include <mutex>
#include <thread>
std::recursive_mutex recursive_mtx;
void recursiveFunction(int n) {
recursive_mtx.lock();
std::cout << "Lock acquired. Depth: " << n << std::endl;
if (n > 0) {
recursiveFunction(n - 1);
}
std::cout << "Unlocking at depth: " << n << std::endl;
recursive_mtx.unlock();
}
int main() {
std::thread t(recursiveFunction, 3);
t.join();
return 0;
}
4. lock_guard && unique_lock
平时我们的lock和unlock()有个风险, 就是中间临界代码如果抛异常了, 那么就不会解锁了, 从而导致一个死锁(因为没解锁)的情况.
所以为了解决这个问题, 有人就设计了一个lockGuard的一个类, 我们之前unlock的问题在于抛异常了就不会执行unlock了, 也就是不会还锁了, 不会还锁那谁都进不来了. 也就是抛异常改变了线程要还锁的一个举动.
这个 lock_guard 是封装引用了 mutex, 算是一个封装了一个类对象的锁, 这玩意很巧妙的结合了域 + 构造 + 析构的用法解决了抛出异常死锁的问题. 这样就可以很巧妙的避免这个尴尬情况, 因为我们明白在cpp语法当中, 类对象是自动销毁的, 自动销毁也就意味着会自动调用析构函数, 而这个lockGuard就是通过析构自动还锁. 程序在临界区抛异常了, 没关系, 这个锁的对象就直接调用析构还锁就好了.
我们下面看一下lock_guard
一个极简的模拟实现:
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
class LockGuard
{
public:
LockGuard(mutex& mtx) :_mtx(mtx)
{
_mtx.lock();
}
~LockGuard()
{
_mtx.unlock();
}
private:
mutex& _mtx;
};
那有些同学说, 我加入说只想在 for 循环所在的这个局部域里去加锁一部分代码呢? 可以吗? 可以, 加个局部作用域即可.
库中的lockGuard不仅仅局限在metex, 库里面用了一个模板, 还可以适配其他的锁, 而不仅仅局限于互斥锁.
另外一个 -> unique_lock
跟lockGuard的区别在于, 支持手动加锁和解锁, 算是一个更加可以自定义的锁守卫~