std::atomic仅保证单变量单次操作的原子性,不保护跨变量业务逻辑一致性;其通过硬件指令(如x86的lock xadd)实现不可分割的读-改-写,但无法消除复合操作竞态。

用 std::atomic 替代裸指针做计数器是最直接的解法
裸指针本身不解决竞态,比如多个线程同时对 *ptr++ 操作,会因读-改-写非原子性导致丢失更新。真正该用的是 std::atomic<int></int> 或更常见的 std::atomic<int></int> —— 它底层通过内存序和硬件指令保证操作原子性,且避免了锁开销。
常见错误是以为把指针声明成 int* const ptr 就线程安全,其实这只是防止指针值被改,*ptr 的读写依然竞态。
- 若只需保护单个整型变量(如计数器),直接用
std::atomic<int></int>,初始化后调用load()/store()/fetch_add() - 若必须用指针(如原子地切换数据结构头节点),才用
std::atomic<t></t>,注意T必须是 trivially copyable - 避免混合使用:不要对同一内存地址既用
std::atomic又用普通指针访问,否则行为未定义
需要共享复杂对象时,std::mutex 配合原始指针更可控
当多个线程要读写一个动态分配的对象(比如 MyClass* obj = new MyClass()),仅靠原子指针只能保证“指针赋值”动作原子,不能保证对象内部成员访问安全。这时候得加锁。
典型陷阱是把 std::mutex 放在对象内部(即“锁内嵌”),结果外部线程先拿到指针、再尝试加锁——但此时对象可能已被销毁,造成 UAF(use-after-free)。
- 推荐将
std::mutex和指针一起封装进管理类,或至少确保锁的生命周期 >= 指针所指对象 - 避免在持有锁期间调用用户代码(如回调函数),以防死锁或锁粒度失控
- 用
std::lock_guard<:mutex></:mutex>自动管理,别手写lock()/unlock()
std::shared_ptr 能解决指针本身的线程安全,但不解决所指对象的竞态
std::shared_ptr 的引用计数是原子的,所以多个线程可同时拷贝或析构它而不会崩溃;但它不保护 get() 返回的原始指针所指向的数据。
错误模式:线程 A 调用 sp.reset(),线程 B 同时执行 sp->do_something() —— 若 reset 触发删除,B 就在访问已释放内存。
- 只要所有线程都只通过
std::shared_ptr访问对象,且不暴露原始指针,引用计数安全就有保障 - 若需原始指针(如传给 C 接口),必须确保该指针生命周期被明确约束,例如配合
std::weak_ptr检查有效性 - 不要用
std::shared_ptr替代同步机制:它不是std::mutex的替代品
调试竞态时,别依赖 printf 或日志打断执行流
加日志看似能定位问题,但 std::cout 本身也是共享资源,且 I/O 会改变线程调度节奏,很可能掩盖原本的竞态——这种现象叫“海森堡 bug”。
真正有效的办法是用工具而非逻辑推断:
- Linux 下优先跑
clang++ -fsanitize=thread编译,TSan 会在运行时报告数据竞争点 - 用
valgrind --tool=helgrind检测锁误用和潜在竞态(虽比 TSan 慢,但兼容性更好) - 避免在生产环境用
std::this_thread::sleep_for()“缓解”竞态,这只是让问题更难复现
最常被忽略的一点:即使所有指针操作都加了锁或用了原子类型,若存在锁顺序不一致(比如线程1锁A再锁B,线程2锁B再锁A),仍会死锁——这不属于竞态,但同样由多线程指针共享引发,且更难调试。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











