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

在 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)。
避免方法 :破坏其中任意一个条件即可,最常见的是所有线程按相同的顺序申请锁(破坏循环等待)。
自我实战操作
bash
cd ~/linux_study
mkdir mutex_test
cd mutex_test
实战 1:灾难现场(不加锁)
bash
vim ticket_no_lock.cc
(输入以下代码)
cpp
#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;
}
编译并运行:
bash
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++ 的析构函数自动解锁,防止我们忘了解锁导致死锁。
bash
vim Lock.hpp
(输入以下代码)
cpp
#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:
cpp
#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 函数和上面一样
重新编译运行:
bash
g++ ticket_lock.cc -o ticket_lock -lpthread
./ticket_lock

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