线程同步机制常见问题解析
写过多线程程序的人,多多少少都踩过坑。比如两个线程同时改一个余额,结果钱算错了;或者某个资源被反复释放,程序直接崩溃。这些问题背后,往往都是线程同步没处理好。
在操作系统或应用开发中,多个线程访问共享资源时,如果不加控制,就会出现数据不一致、竞态条件(race condition)等问题。这时候就得靠线程同步机制来协调。但用得不对,反而会引入新麻烦。
1. 死锁:互相等待的僵局
最典型的场景是两个线程各自拿着一把锁,又等着对方手里的那把。比如线程A持有锁1,想拿锁2;线程B拿着锁2,却在等锁1。谁也不放手,程序就卡死了。
这种情况在生活中就像两个人在窄走廊迎面走来,都想让对方先退,结果谁都没动。解决办法是统一加锁顺序,比如大家都先申请锁1再申请锁2,避免交叉等待。
2. 忘记加锁:共享变量成“公共黑板”
有些开发者觉得“读操作不用锁”,于是多个线程同时读写一个计数器,结果数值错乱。哪怕只是读,如果和其他写操作并行,也可能读到中间状态。
举个例子,银行账户余额正在从1000减到800,另一个线程刚好读到900——这不是真实存在的金额,而是修改过程中的临时值。这种“脏读”必须靠锁来避免。
3. 锁的粒度太粗或太细
锁得太狠,比如整个函数套一个大锁,会导致并发性能下降,线程排队等得不耐烦。反过来,锁得太松,比如每个变量都单独加锁,管理复杂,容易漏锁或死锁。
合适的粒度要看场景。比如缓存系统里,可以按缓存槽位分段加锁,既保证隔离性,又提升并发能力。
4. 忙等待浪费CPU
有人用while循环不断检查某个标志位是否改变,这种方式叫忙等待。线程一直在运行,占用CPU时间,却不干活,白白耗电。
更好的做法是使用条件变量(condition variable),让线程进入等待状态,直到被通知唤醒。这样既省资源,又及时响应。
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker_thread() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
// 开始工作
}5. 误用原子操作代替锁
原子操作适合简单变量,比如计数器增减。但它不能替代锁处理复杂逻辑。比如要同时更新两个关联变量,原子操作无法保证整体事务性。
就像转账操作:从A扣钱和给B加钱必须一起完成,否则一半成功一半失败,账就乱了。这种场景还得靠互斥锁保护临界区。
6. 忘记异常安全
加锁之后,代码中途抛异常,如果没有用RAII(如C++的lock_guard)自动释放锁,就会导致锁一直不被释放,其他线程永远等下去。
下面这种写法就很危险:
mtx.lock();
do_something(); // 这里抛异常,锁就没了
mtx.unlock();应该改用自动管理的方式:
{
std::lock_guard<std::mutex> guard(mtx);
do_something(); // 即使抛异常,析构时也会自动解锁
}线程同步不是加个锁就万事大吉。设计时要考虑全面:有没有死锁风险?锁的范围合不合适?是否支持异常安全?只有把这些细节抠清楚,多线程程序才能跑得稳又高效。