马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
线程大部分时候用的是自己栈上的局部变量,别的线程碰不到。但总有些数据要在线程之间流转,这种变量叫共享变量。一旦多个线程同时读写同一个共享变量,问题就来了——这篇用一个抢票的例子把互斥这件事讲清楚。
一、为什么需要互斥
局部变量放在线程栈里,天然归一个线程所有;共享变量放在全局或堆上,所有线程都够得着。线程之间靠共享变量实现交互的同时,也把竞态风险一起引进来了。
用一个全局变量 ticket 表示余票,起多个线程同时抢。以 5 个线程、100 张票为例,核心逻辑是这样:
#define NUM 5
int ticket = 100;
class threadData
{
public:
threadData(int number)
{
threadname = "thread-" + to_string(number);
}
public:
string threadname;
};
void* getTicket(void* args)
{
threadData* td = static_cast<threadData*>(args);
while (1)
{
if (ticket > 0)
{
usleep(1000); // 模拟抢票耗时
printf("%s is running, get a ticket: %d\n", td->threadname.c_str(), ticket);
ticket--;
}
else
{
break;
}
}
return nullptr;
}
二、问题出在哪一行
危险的是 if (ticket > 0) 与 ticket-- 这两句不是原子的。实际执行时是「读 ticket → 判断 → 自减 → 写回」,中间完全可能被别的线程插进来:
- 线程 A 读到 ticket=1,判断通过;
- 还没来得及自减,线程 B 也读到 ticket=1,同样判断通过;
- A 写回 0,B 也写回 0;
- 两张票被两个线程当成一张卖掉了。
再叠加 usleep 这种主动让出 CPU 的调用,窗口被放得更大——现象就是票数会出现重复编号,或者卖到 0 之后仍有人抢到。
三、互斥量的解法
办法很直接:把「判断 + 修改」整段包进互斥锁,同一时刻只允许一个线程进入。每个线程持自己的锁指针:
void* getTicket(void* args)
{
threadData* td = static_cast<threadData*>(args);
pthread_mutex_t* lock = td->_lock;
while (1)
{
pthread_mutex_lock(lock);
if (ticket > 0)
{
usleep(1000);
printf("%s is running, get a ticket: %d\n", td->_threadname.c_str(), ticket);
ticket--;
}
else
{
pthread_mutex_unlock(lock);
break;
}
pthread_mutex_unlock(lock);
}
return nullptr;
}
四、写mutex代码的四条硬规则
- 临界区要小:锁里面只放必要的判断和自增自减,
printf、耗时的_io 尽量挪到锁外,否则并发度会被锁打死。
- 每个出口都要解锁:循环里
break 那条分支容易漏,一漏就是死锁全局线程。
- 用 RAII 或 try/finally 兜底:C++ 里更推荐
std::lock_guard<std::mutex>,作用域结束自动解锁,从根本上杜绝漏解。
- 别在持锁时再申请另一把锁:多把锁很容易出现 A 等 B、B 等 A 的死锁。
五、和信号量的区别,一句话
互斥量强调「谁进来谁出去」的排他性,适合保护临界资源;信号量强调「同时允许多少个」的计数,适合限流。抢票这种同一份数据只能一个人改的场景,用互斥。
结论
共享变量是线程通信的桥,也是竞态的源。判断一个程序有没有互斥问题,就看有没有「先判断后修改」且中间可能被打断的结构。修法恒定:把判断加修改整段锁起来,锁内只做最小必要操作,所有退出路径都要解锁——C++ 里直接用 lock_guard。
qidao123.com企服评测-应用市场 编辑提示 |