1. 前言
在我们之前学习的代码中,构建多线程时可能会出现乱码或抢占输出。本章将探讨其原因。
- 分析示例问题所在。
- 理解线程互斥概念。
- 认识互斥锁机制。
- 掌握互斥锁使用方法。
2. 买票示例
假设有 100 张电影票,同时抢票会出现什么问题?
#include <iostream>
#include <thread>
#include <vector>
#include <string>
#include <cstdio>
#include <unistd.h>
int ticket = 100;
void routine(std::string name) {
while (1) {
if (ticket > 0) {
usleep(1000);
ticket--;
printf("%s sell ticket, now tickets number:%d\n", name.c_str(), ticket);
} else {
std::cout << ticket << std::endl;
break;
}
}
return;
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; i++) {
std::string name = "thread-";
name += std::to_string(i);
threads.emplace_back(routine, name);
}
for (auto& thread : threads) {
thread.join();
}
return 0;
}
公共资源 ticket 被五个线程竞争。使用 usleep(1000) 模拟抢票耗时。理论上票数为 0 时应停止,但运行结果可能为负数。
2-1 原因
单线程不会发生此问题,多线程存在竞争。每个线程读取 ticket 值后休息,再减减。
当票数为 1 时:线程 1 读取到 1,休息;线程 2 启动,也读取到 1,休息。线程 1 醒来执行减减变为 0。线程 2 醒来基于之前的判断继续减减,变为 -1。
这是经典的 check-then-act race(检查后执行竞态)。汇编层面表现为:判断时加载寄存器 R1,减法时重新加载内存到 R2,导致数据不一致。
3. 引入锁的概念
为防止上述问题,引入锁机制。
#include <iostream>
#include <thread>
#include <vector>
#include <string>
#include <cstdio>
#include <unistd.h>
int ticket = 100;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void routine(std::string name) {
while (1) {
pthread_mutex_lock(&lock);
if (ticket > 0) {
usleep(1000);
ticket--;
printf("%s sell ticket, now tickets number:%d\n", name.c_str(), ticket);
pthread_mutex_unlock(&lock);
} else {
pthread_mutex_unlock(&lock);
break;
}
}
return;
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; i++) {
std::string name = "thread-";
name += std::to_string(i);
threads.emplace_back(routine, name);
}
for (auto& thread : threads) {
thread.join();
}
return 0;
}
运行正常,完成了检票任务。这里使用的是互斥锁 (Mutex)。
- 特点: 同一时间只有一个线程能持有锁。
- 用法:
pthread_mutex_lock()加锁,pthread_mutex_unlock()解锁。
3-1 互斥锁上锁的位置
访问公共资源的地方都需要上锁,即临界区 (Critical Section)。
- 定义: 代码中访问共享资源(全局变量、外部文件等)的部分。
- 规则: 同一时刻只允许一个线程进入。
- 后果: 不保护会发生'竞态条件',导致数据毁坏。
- 方式: 进入前加锁,离开后解锁。
3-2 解锁的时机
代码中无论 if 还是 else 都要解锁。若直接 break 而不解锁,会导致死锁。
- 死锁诱因: 线程持有锁退出(break/return/异常),未释放锁,其他线程永久等待。
- Coffman 条件: 互斥、占有并等待、不可剥夺、循环等待。
3-3 线程拿着锁睡觉
usleep 不应在锁内执行,否则浪费 CPU 资源给其他线程。
优化后的代码应将 usleep 移出临界区,或仅在获取锁后短暂操作。
// 优化建议:usleep 放在锁外,或仅在必要时休眠
void routine(std::string name) {
while (1) {
pthread_mutex_lock(&lock);
if (ticket > 0) {
ticket--;
printf("%s sell ticket, now tickets number:%d\n", name.c_str(), ticket);
pthread_mutex_unlock(&lock);
} else {
pthread_mutex_unlock(&lock);
break;
}
}
return;
}
3-4 公平性问题
即使有锁,也可能出现某个线程反复抢到锁的情况(如 thread-2)。互斥锁保证互斥,但不保证公平调度。
4. 总结
本文从票数变负的现象出发,分析了多线程中的竞态条件(Race Condition)。核心原因是 check-then-act 逻辑下,线程间对共享资源的非原子访问。通过互斥锁(Mutex)可以确保临界区的互斥访问,防止数据错乱。使用时需注意锁的范围,避免包含耗时操作(如 sleep),并确保所有分支路径都能正确解锁以防死锁。此外,互斥锁不提供公平性保障,线程调度仍取决于操作系统。掌握临界区管理与锁粒度控制是编写安全并发程序的基础。


