☰
Linux 学习笔记(三十三):线程互斥——互斥锁(Mutex)与死锁
2026/10/28 19:02:26 网站建设 项目流程

一、 为什么多线程修改全局变量会出错?


在 C/C++ 中,ticket--并不是一个原子操作。它在汇编层面对应三条指令(load, update, store)。
当多个线程并发执行时,CPU 的时间片切换可能发生在任意一步。这就导致线程 A 读了数据还没写回,线程 B 又去读,最终互相覆盖。这种现象叫做竞态条件(Race Condition)。

二、 互斥锁(Mutex)

为了解决并发冲突,引入了互斥锁。它保证了在任意时刻,只能有一个线程进入临界区(访问共享资源的代码)。

  • pthread_mutex_lock():加锁。如果已被别人加锁,当前线程阻塞。

  • pthread_mutex_unlock():解锁。

三、 高阶封装:RAII 风格的锁守卫


手动调用 lock 和 unlock 极易因为程序抛出异常或中途break导致“死锁”。
C++ 采用 RAII(资源获取即初始化)机制:在构造函数中加锁,在析构函数中解锁。只要对象生命周期结束,就一定会解锁。极其优雅且安全。

四、 死锁的四个必要条件

用锁不当会导致死锁(两个线程互等)。死锁发生必须具备四个条件:

  1. 互斥:资源同一时间只能被一个线程使用。

  2. 请求与保持:拿着一个锁,去等另一个锁,且不释放手里的。

  3. 不剥夺:别人不能强行抢锁。

  4. 循环等待:存在一个线程资源等待的环形链(A等B,B等A)。
    避免方法:破坏其中任意一个条件即可,最常见的是所有线程按相同的顺序申请锁(破坏循环等待)。

自我实战操作

cd ~/linux_study mkdir mutex_test cd mutex_test
实战 1:灾难现场(不加锁)
vim ticket_no_lock.cc

(输入以下代码)

#include <iostream> #include <pthread.h> #include <unistd.h> int ticket = 100; // 100张票 // 抢票动作 void* route(void* arg) { char* name = (char*)arg; while (true) { if (ticket > 0) { usleep(1000); // 模拟抢票时的网络延迟等耗时操作 printf("%s 卖出一张票,剩余: %d\n", name, ticket); ticket--; } else { break; } } return nullptr; } int main() { pthread_t t1, t2, t3, t4; pthread_create(&t1, NULL, route, (void*)"Thread-1"); pthread_create(&t2, NULL, route, (void*)"Thread-2"); pthread_create(&t3, NULL, route, (void*)"Thread-3"); pthread_create(&t4, NULL, route, (void*)"Thread-4"); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_join(t4, NULL); return 0; }

编译并运行:

g++ ticket_no_lock.cc -o ticket_no_lock -lpthread ./ticket_no_lock | tail -n 10 # 只看最后10行

👉现象:你会看到最后卖出的票数变成了0、-1、-2甚至更低。这就是典型的多线程数据冲突。

实战 2:引入 RAII 互斥锁

极其标准的Lock.hpp封装,它利用了 C++ 的析构函数自动解锁,防止我们忘了解锁导致死锁。

vim Lock.hpp

(输入以下代码)

#pragma once #include <pthread.h> class Mutex { public: Mutex() { pthread_mutex_init(&_mutex, nullptr); } ~Mutex() { pthread_mutex_destroy(&_mutex); } void Lock() { pthread_mutex_lock(&_mutex); } void Unlock() { pthread_mutex_unlock(&_mutex); } private: pthread_mutex_t _mutex; // 禁止拷贝 Mutex(const Mutex&) = delete; const Mutex& operator=(const Mutex&) = delete; }; // RAII 风格的锁守卫 class LockGuard { public: LockGuard(Mutex& mutex) : _mutex(mutex) { _mutex.Lock(); } ~LockGuard() { _mutex.Unlock(); } private: Mutex& _mutex; };
实战 3:完美解决冲突

修改刚才的抢票代码,引入Lock.hpp:

#include <iostream> #include <pthread.h> #include <unistd.h> #include "Lock.hpp" // 引入封装好的锁 int ticket = 100; Mutex mtx; // 全局的互斥锁 void* route(void* arg) { char* name = (char*)arg; while (true) { // 利用 RAII,在进入 if 之前加锁,离开作用域(或 break)时自动解锁 LockGuard guard(mtx); if (ticket > 0) { usleep(1000); printf("%s 卖出一张票,剩余: %d\n", name, ticket); ticket--; } else { break; } } return nullptr; } // main 函数和上面一样

重新编译运行:

g++ ticket_lock.cc -o ticket_lock -lpthread ./ticket_lock

👉现象:票数完美变成 0 结束,没有任何负数,且 4 个线程井然有序。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询