一、 为什么多线程修改全局变量会出错?
在 C/C++ 中,ticket--并不是一个原子操作。它在汇编层面对应三条指令(load, update, store)。
当多个线程并发执行时,CPU 的时间片切换可能发生在任意一步。这就导致线程 A 读了数据还没写回,线程 B 又去读,最终互相覆盖。这种现象叫做竞态条件(Race Condition)。
二、 互斥锁(Mutex)
为了解决并发冲突,引入了互斥锁。它保证了在任意时刻,只能有一个线程进入临界区(访问共享资源的代码)。
pthread_mutex_lock():加锁。如果已被别人加锁,当前线程阻塞。pthread_mutex_unlock():解锁。
三、 高阶封装:RAII 风格的锁守卫
手动调用 lock 和 unlock 极易因为程序抛出异常或中途break导致“死锁”。
C++ 采用 RAII(资源获取即初始化)机制:在构造函数中加锁,在析构函数中解锁。只要对象生命周期结束,就一定会解锁。极其优雅且安全。
四、 死锁的四个必要条件
用锁不当会导致死锁(两个线程互等)。死锁发生必须具备四个条件:
互斥:资源同一时间只能被一个线程使用。
请求与保持:拿着一个锁,去等另一个锁,且不释放手里的。
不剥夺:别人不能强行抢锁。
循环等待:存在一个线程资源等待的环形链(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 个线程井然有序。