05 | C++ 线程同步之互斥锁
解决多线程数据混乱的方案就是进行线程同步,最常用的就是互斥锁,在 C++11 中一共提供了四种互斥锁:
- std::mutex:独占的互斥锁,不能递归使用
- std::timed_mutex:带超时的独占互斥锁,不能递归使用
- std::recursive_mutex:递归互斥锁,不带超时功能
- std::recursive_timed_mutex:带超时的递归互斥锁
互斥锁在有些资料中也被称之为互斥量,二者是一个东西
std::mutex
成员函数
lock() 函数用于给临界区加锁,并且只能有一个线程获得锁的所有权,它有阻塞线程的作用,函数原型如下:
void lock();
除了使用 lock() 还可以使用 try_lock() 获取互斥锁的所有权并对互斥锁加锁,函数原型如下:
bool try_lock(); ```二者的区别在于 try_lock() 不会阻塞线程,lock() 会阻塞线程: + 如果互斥锁是未锁定状态,得到了互斥锁所有权并加锁成功,函数返回 true + 如果互斥锁是锁定状态,无法得到互斥锁所有权加锁失败,函数返回 false 当互斥锁被锁定之后可以通过 unlock() 进行解锁,但是需要注意的是只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁,其它线程是没有权限做这件事情的。该函数的函数原型如下:
void unlock();
**线程同步的目的是让多线程按照顺序依次执行临界区代码,这样做线程对共享资源的访问就从并行访问变为了线性访问,访问效率降低了,但是保证了数据的正确性。** ## 线程同步 举个栗子,我们让两个线程共同操作同一个全局变量,二者交替数数,将数值存储到这个全局变量里边并打印出来。
include
include
include
include
using namespace std;
int g_num = 0; // 被 g_num_mutex 所保护
mutex g_num_mutex;
void slow_increment(int id)
{
for (int i = 0; i < 3; ++i)
{
g_num_mutex.lock();
++g_num;
cout << id << " => " << g_num << endl;
g_num_mutex.unlock();
this_thread::sleep_for(chrono::seconds(1)); }
}
int main()
{
thread t1(slow_increment, 0);
thread t2(slow_increment, 1);
t1.join();
t2.join();
getchar();
}
 + 在所有线程的任务函数执行完毕之前,互斥锁对象是不能被析构的,一定要在程序中保证这个对象的可用性。 + 互斥锁的个数和共享资源的个数相等,也就是说每一个共享资源都应该对应一个互斥锁对象。互斥锁对象的个数和线程的个数没有关系。 # std::lock_guard --- lock_guard 是 C++11 新增的一个模板类,使用这个类,可以简化互斥锁 lock() 和 unlock() 的写法,同时也更安全。这个模板类的定义和常用的构造函数原型如下:
// 类的定义,定义于头文件
template< class Mutex >
class lock_guard;
// 常用构造函数
explicit lock_guard( mutex_type& m );
lock_guard 在使用上面提供的这个构造函数构造对象时,会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记 unlock() 操作而导致线程死锁。lock_guard **使用了 RAII 技术**,就是在类构造函数中分配资源,在析构函数中释放资源,保证资源出了作用域就释放。 使用 lock_guard 对上面的例子进行修改,代码如下:
void slow_increment(int id)
{
for (int i = 0; i < 3; ++i)
{
// 使用哨兵锁管理互斥锁
lock_guard
++g_num;
cout << id << " => " << g_num << endl;
this_thread::sleep_for(chrono::seconds(1));
}
}
通过修改发现代码被精简了,而且不用担心因为忘记解锁而造成程序的死锁,但是这种方式也有弊端,在上面的示例程序中整个for循环的体都被当做了临界区,多个线程是线性的执行临界区代码的,因此临界区越大程序效率越低,还是需要根据实际情况选择最优的解决方案。 # std::recursive_mutex --- **递归互斥锁 std::recursive_mutex 允许同一线程多次获得互斥锁**,可以用来解决同一线程需要多次获取互斥量时死锁的问题,在下面的例子中使用独占非递归互斥量会发生死锁:
include
include
include
using namespace std;
struct Calculate
{
Calculate() : m_i(6) {}
void mul(int x) { lock_guard<recursive_mutex> locker(m_mutex); m_i *= x; } void div(int x) { lock_guard<recursive_mutex> locker(m_mutex); m_i /= x; } void both(int x, int y) { lock_guard<recursive_mutex> locker(m_mutex); mul(x); div(y); } int m_i; recursive_mutex m_mutex;
};
int main()
{
Calculate cal;
cal.both(6, 3);
cout << "cal.m_i = " << cal.m_i << endl;
return 0;
}
虽然递归互斥锁可以解决同一个互斥锁频繁获取互斥锁资源的问题,但是还是建议少用,主要原因如下: + **使用递归互斥锁的场景往往都是可以简化的**,使用递归互斥锁很容易放纵复杂逻辑的产生,从而导致bug的产生 + 递归互斥锁比非递归互斥锁效率要低一些。 + 递归互斥锁虽然允许同一个线程多次获得同一个互斥锁的所有权,但最大次数并未具体说明,一旦超过一定的次数,就会抛出std::system错误。 # std::timed_mutex --- **std::timed_mutex 是超时独占互斥锁**,主要是在获取互斥锁资源时增加了超时等待功能,因为不知道获取锁资源需要等待多长时间,**为了保证不一直等待下去,设置了一个超时时长**,超时后线程就可以解除阻塞去做其他事情了。 std::timed_mutex 比 std::_mutex 多了两个成员函数:try_lock_for() 和 try_lock_until():
void lock();
bool try_lock();
void unlock();
// std::timed_mutex比std::_mutex多出的两个成员函数
template <class Rep, class Period>
bool try_lock_for (const chrono::duration<Rep,Period>& rel_time);
template <class Clock, class Duration>
bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);
+ try_lock_for 函数是当线程获取不到互斥锁资源的时候,让线程阻塞一定的时间长度 + try_lock_until 函数是当线程获取不到互斥锁资源的时候,让线程阻塞到某一个指定的时间点 + 关于两个函数的返回值:当得到互斥锁的所有权之后,函数会马上解除阻塞,返回 true,如果阻塞的时长用完或者到达指定的时间点之后,函数也会解除阻塞,返回 false
include
include
include
using namespace std;
timed_mutex g_mutex;
void work()
{
chrono::seconds timeout(1);
while (true)
{
// 通过阻塞一定的时长来争取得到互斥锁所有权
if (g_mutex.try_lock_for(timeout))
{
cout << "当前线程ID: " << this_thread::get_id()
<< ", 得到互斥锁所有权..." << endl;
// 模拟处理任务用了一定的时长
this_thread::sleep_for(chrono::seconds(10));
// 互斥锁解锁
g_mutex.unlock();
break;
}
else
{
cout << "当前线程ID: " << this_thread::get_id()
<< ", 没有得到互斥锁所有权..." << endl;
// 模拟处理其他任务用了一定的时长
this_thread::sleep_for(chrono::milliseconds(50));
}
}
}
int main()
{
thread t1(work);
thread t2(work);
t1.join(); t2.join(); return 0;
}
在上面的例子中,通过一个 while 循环不停的去获取超时互斥锁的所有权,如果得不到就阻塞 1 秒钟,1 秒之后如果还是得不到阻塞 50 毫秒,然后再次继续尝试,直到获得互斥锁的所有权,跳出循环体。 关于递归超时互斥锁 std::recursive_timed_mutex 的使用方式和 std::timed_mutex 是一样的,只不过它可以允许一个线程多次获得互斥锁所有权,而 std::timed_mutex 只允许线程获取一次互斥锁所有权。另外,递归超时互斥锁 std::recursive_timed_mutex 也拥有和 std::recursive_mutex 一样的弊端,不建议频繁使用。 # 参考 爱编程的大丙 https://subingwen.cn/cplusplus/
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~