【C++】零基础入门 · 第 18 节:互斥锁与线程同步
2026/6/2 15:50:01 网站建设 项目流程

在第 17 节中,我们学习了多线程编程的基础概念和std::thread的使用。本节将深入讲解线程同步的核心工具——互斥锁(Mutex),解决多线程编程中最常见的问题:数据竞争

1. 数据竞争:多线程的头号敌人

1.1 什么是数据竞争?

当两个或多个线程同时读写同一块内存,且至少有一个线程在写入时,就会发生数据竞争(Data Race)。数据竞争的结果是未定义行为,程序可能崩溃、产生错误结果,或者看似正常但偶尔出问题。

#include<iostream>#include<thread>usingnamespacestd;intcounter=0;voidincrement(){for(inti=0;i<100000;i++){counter++;// 这不是原子操作!}}intmain(){threadt1(increment);threadt2(increment);t1.join();t2.join();// 期望 200000,实际结果不确定cout<<"counter = "<<counter<<endl;return0;}

运行多次,结果可能为 134567、178234 等各种随机值,永远不是 200000。

1.2 为什么 counter++ 不安全?

counter++看似一条语句,实际包含三个步骤:

  1. 读取:从内存读取 counter 的值到寄存器
  2. 修改:在寄存器中将值加 1
  3. 写回:将新值写回内存

如果两个线程同时执行,可能的交错情况:

线程A:读取 counter = 100 线程B:读取 counter = 100 线程A:写入 counter = 101 线程B:写入 counter = 101 ← 丢失了一次自增!

2. std::mutex:最基本的互斥锁

2.1 什么是互斥锁?

互斥锁(Mutex,Mutual Exclusion 的缩写)就像一把门锁。当一个线程进入临界区时,它把锁锁上,其他线程必须等待锁被释放才能进入。

线程A:加锁 → 访问共享数据 → 解锁 线程B:等待... → 加锁 → 访问共享数据 → 解锁

2.2 修复数据竞争

#include<iostream>#include<thread>#include<mutex>usingnamespacestd;intcounter=0;mutex mtx;voidincrement(){for(inti=0;i<100000;i++){mtx.lock();counter++;mtx.unlock();}}intmain(){threadt1(increment);threadt2(increment);t1.join();t2.join();cout<<"counter = "<<counter<<endl;// 总是 200000return0;}

运行结果:

counter = 200000

2.3 lock() 和 unlock() 的注意事项

手动调用lock()unlock()有一个致命问题:如果在lock()之后、unlock()之前发生异常,锁永远不会被释放,导致死锁

voidriskyFunction(){mtx.lock();// 如果这里抛出异常...doSomething();// ← 异常!mtx.unlock();// ← 永远不会执行!}

3. std::lock_guard:RAII 自动锁

3.1 什么是 lock_guard?

std::lock_guard是一个 RAII 风格的锁管理器。它在构造时加锁,在析构时自动解锁,即使发生异常也能保证锁被释放。

#include<iostream>#include<thread>#include<mutex>usingnamespacestd;intcounter=0;mutex mtx;voidincrement(){for(inti=0;i<100000;i++){lock_guard<mutex>lock(mtx);// 构造时加锁counter++;// 离开作用域时自动解锁}}intmain(){threadt1(increment);threadt2(increment);t1.join();t2.join();cout<<"counter = "<<counter<<endl;// 200000return0;}

3.2 作用域决定锁的范围

lock_guard的生命周期就是锁的范围。可以用花括号控制:

voidexample(){cout<<"这里没有锁"<<endl;{lock_guard<mutex>lock(mtx);cout<<"这里是临界区,锁生效"<<endl;// 在花括号结束时自动解锁}cout<<"锁已经释放"<<endl;}

4. std::unique_lock:更灵活的锁

4.1 与 lock_guard 的区别

std::unique_locklock_guard更灵活,支持:

  • 延迟加锁:构造时不立即加锁
  • 手动解锁和重新加锁unlock()lock()
  • 转移所有权:配合条件变量使用
#include<iostream>#include<thread>#include<mutex>usingnamespacestd;mutex mtx;voidflexibleLocking(){unique_lock<mutex>lock(mtx,defer_lock);// 不立即加锁cout<<"做一些不需要锁的操作..."<<endl;lock.lock();// 现在加锁cout<<"临界区操作"<<endl;lock.unlock();// 手动解锁cout<<"做一些不需要锁的操作..."<<endl;lock.lock();// 可以再次加锁cout<<"再次进入临界区"<<endl;}intmain(){threadt1(flexibleLocking);t1.join();return0;}

4.2 延迟加锁(defer_lock)

// 方式一:构造时加锁(默认)unique_lock<mutex>lock1(mtx);// 方式二:延迟加锁unique_lock<mutex>lock2(mtx,defer_lock);lock2.lock();// 需要时再加锁// 方式三:尝试加锁(不阻塞)unique_lock<mutex>lock3(mtx,try_to_lock);if(lock3.owns_lock()){// 成功获取锁}

4.3 lock_guard vs unique_lock

特性lock_guardunique_lock
构造时加锁必须可选
手动 unlock/lock不支持支持
转移所有权不支持支持
性能略快略慢(更多检查)
使用场景简单临界区复杂同步需求

建议:简单场景用lock_guard,需要灵活性时用unique_lock

5. std::scoped_lock(C++17)

5.1 同时锁多个互斥量

std::scoped_lock可以同时锁住多个互斥量,避免死锁。

#include<iostream>#include<thread>#include<mutex>usingnamespacestd;mutex mtx1,mtx2;voidthreadA(){scoped_locklock(mtx1,mtx2);// 同时锁住两个锁cout<<"线程A:同时持有两把锁"<<endl;}voidthreadB(){scoped_locklock(mtx1,mtx2);// 相同顺序,不会死锁cout<<"线程B:同时持有两把锁"<<endl;}intmain(){threadt1(threadA);threadt2(threadB);t1.join();t2.join();return0;}

5.2 为什么 scoped_lock 能避免死锁?

如果手动按不同顺序加锁:

// 线程Amtx1.lock();mtx2.lock();// 死锁风险!// 线程Bmtx2.lock();mtx1.lock();// 死锁!

scoped_lock内部使用死锁避免算法,无论你以什么顺序传入,都能安全加锁。

6. 读写锁:std::shared_mutex(C++17)

6.1 读多写少的场景

在很多场景中,读操作远多于写操作。如果用普通互斥锁,读操作之间也会互斥,浪费性能。

std::shared_mutex支持:

  • 共享锁(读锁):多个线程可以同时持有
  • 独占锁(写锁):只有一个线程能持有
#include<iostream>#include<thread>#include<shared_mutex>#include<vector>usingnamespacestd;shared_mutex rwMutex;intsharedData=0;voidreader(intid){shared_lock<shared_mutex>lock(rwMutex);// 共享锁cout<<"读者 "<<id<<" 读取数据:"<<sharedData<<endl;}voidwriter(intid,intvalue){unique_lock<shared_mutex>lock(rwMutex);// 独占锁sharedData=value;cout<<"写者 "<<id<<" 写入数据:"<<value<<endl;}intmain(){vector<thread>threads;// 启动多个读者和写者for(inti=0;i<5;i++){threads.emplace_back(reader,i);}threads.emplace_back(writer,1,100);threads.emplace_back(writer,2,200);for(inti=5;i<10;i++){threads.emplace_back(reader,i);}for(auto&t:threads)t.join();return0;}

6.2 shared_lock 和 unique_lock 的配合

shared_mutex rwMutex;// 读操作:使用 shared_lockvoidreadData(){shared_lock<shared_mutex>lock(rwMutex);// 多个线程可以同时执行这里readFromDatabase();}// 写操作:使用 unique_lockvoidwriteData(){unique_lock<shared_mutex>lock(rwMutex);// 只有一个线程能执行这里writeToDatabase();}

7. 原子操作:std::atomic

7.1 什么是原子操作?

对于简单的计数器等场景,使用互斥锁有点重。std::atomic提供了无锁的线程安全操作。

#include<iostream>#include<thread>#include<atomic>usingnamespacestd;atomic<int>counter(0);voidincrement(){for(inti=0;i<100000;i++){counter++;// 原子操作,线程安全}}intmain(){threadt1(increment);threadt2(increment);t1.join();t2.join();cout<<"counter = "<<counter<<endl;// 总是 200000return0;}

7.2 atomic 的常用操作

atomic<int>a(0);a.store(10);// 写入intval=a.load();// 读取a.exchange(20);// 交换,返回旧值// 复合操作a.fetch_add(5);// 原子加a.fetch_sub(3);// 原子减a.fetch_and(0xFF);// 原子与a.fetch_or(0x01);// 原子或// 前缀/后缀自增自减a++;++a;a--;--a;// 比较并交换(CAS)intexpected=10;a.compare_exchange_strong(expected,20);// 如果 a == 10,则 a = 20,返回 true// 如果 a != 10,则 expected = a 的值,返回 false

7.3 atomic vs mutex

特性atomicmutex
适用范围单个变量代码块
性能非常快(无锁)较慢(有锁)
复杂操作不支持支持
死锁风险

建议:简单计数器、标志位用atomic,复杂数据结构用mutex

8. 常见陷阱与最佳实践

8.1 不要返回引用给共享数据

// 错误:返回引用后,锁已释放,调用者访问的是未保护的数据int&getCounter(){lock_guard<mutex>lock(mtx);returncounter;// 危险!}// 正确:返回副本intgetCounter(){lock_guard<mutex>lock(mtx);returncounter;// 返回拷贝}

8.2 锁的粒度要合适

// 粒度太粗:整个函数都被锁住voidbad(){lock_guard<mutex>lock(mtx);doSlowComputation();// 这部分不需要锁updateSharedData();// 只有这里需要锁}// 粒度合适:只锁必要的部分voidgood(){doSlowComputation();// 无锁lock_guard<mutex>lock(mtx);updateSharedData();// 有锁}

8.3 避免嵌套锁

// 危险:可能导致死锁voiddangerous(){lock_guard<mutex>lock1(mtxA);lock_guard<mutex>lock2(mtxB);// 如果其他线程反序加锁 → 死锁}// 安全:使用 scoped_lockvoidsafe(){scoped_locklock(mtxA,mtxB);// 自动避免死锁}

9. 总结

本节我们学习了线程同步的核心工具:

  • 数据竞争:多线程同时读写共享数据导致的未定义行为
  • std::mutex:基本互斥锁,lock()/unlock()
  • std::lock_guard:RAII 自动锁,简单场景首选
  • std::unique_lock:灵活锁,支持延迟加锁和条件变量
  • std::scoped_lock(C++17):同时锁多个锁,避免死锁
  • std::shared_mutex(C++17):读写锁,适合读多写少场景
  • std::atomic:无锁原子操作,适合简单变量

下一节我们将学习条件变量,它是线程间通信的核心工具,用于实现生产者-消费者模型等经典并发模式。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询