【Linux系统编程】(四十七)线程安全与线程锁深度解析:从概念到实战,避坑指南全掌握

📅 发布时间:2026/7/11 11:05:34 👁️ 浏览次数:
【Linux系统编程】(四十七)线程安全与线程锁深度解析:从概念到实战,避坑指南全掌握
前言在多线程编程的世界里线程安全是永恒的核心话题而线程锁则是保障线程安全的核心武器。当多个线程并发访问共享资源时稍不注意就会出现数据竞争、结果错乱甚至程序崩溃的问题而锁的不当使用又会引发死锁、性能损耗等新问题。除此之外函数重入、STL 容器和智能指针的线程安全特性也是多线程开发中极易踩坑的点。本文将从线程安全与可重入的核心概念出发深入剖析死锁的成因与解决办法再结合 STL 和智能指针的线程安全特性展开讲解全程搭配 C/C 实战代码让你从原理到实战彻底掌握线程安全与线程锁的核心知识告别多线程编程的各种坑下面就让我们正式开始吧一、线程安全与可重入分清概念避免混淆很多开发者在接触多线程编程时会把线程安全和可重入混为一谈认为二者是同一个概念。实际上二者既有紧密联系又有本质区别。理解这两个概念是编写安全的多线程代码的基础。1.1 核心概念定义1线程安全线程安全指的是多个线程并发访问同一段代码或共享资源时程序能够正确执行不会出现结果错乱、数据损坏或执行流程异常的情况。简单来说线程安全的代码在多线程环境下的执行结果和单线程环境下的执行结果完全一致。反之如果多线程并发执行时对全局变量、静态变量等共享资源的操作没有任何保护措施就极易出现线程不安全的问题。线程不安全的典型场景未加锁保护的共享变量操作如多线程抢票案例中的ticket--函数状态会随调用发生变化的情况返回指向静态变量指针的函数多个线程会访问同一个静态变量调用了其他线程不安全函数的函数。线程安全的典型场景代码只使用局部变量局部变量存储在线程栈中属于线程私有对全局 / 静态变量只有读权限无写权限对共享资源的操作都是原子操作多个线程的切换不会导致接口执行结果出现二义性。2可重入重入指的是同一个函数被不同的执行流调用当前一个执行流还未执行完该函数时另一个执行流又再次进入该函数。而可重入函数则是指函数在重入的情况下运行结果不会出现任何异常或不同反之则为不可重入函数。重入主要分为两种场景多线程重入多个线程同时调用同一个函数信号导致的重入程序在执行函数的过程中被信号中断信号处理函数又调用了原函数。不可重入的典型场景函数内部调用了malloc/freemalloc通过全局链表管理堆内存并发调用会导致链表错乱函数内部调用了标准 I/O 库函数标准 I/O 库的很多实现使用了全局数据结构是非重入的函数体内使用了静态 / 全局数据结构多个执行流会同时修改该数据结构函数未释放锁就再次进入会导致死锁后续重点讲解。可重入的典型场景不使用全局变量、静态变量仅使用局部变量不调用malloc/new或free/delete避免操作全局堆管理结构不返回静态 / 全局数据所有数据由函数调用者提供不调用任何不可重入的函数通过制作全局数据的本地拷贝来保护全局数据。1.2 线程安全与可重入的联系与区别1核心联系可重入函数一定是线程安全的这是二者最核心的关联。因为可重入函数的设计本身就保证了多个执行流同时调用时不会出现资源竞争自然能在多线程环境下安全执行。反之如果一个函数是不可重入的那么它一定不能被多个线程并发调用否则大概率会引发线程安全问题。另外如果一个函数中有全局 / 静态变量且未做任何保护那么这个函数既不是线程安全的也不是可重入的。2本质区别二者的核心区别在于关注的侧重点不同线程安全侧重描述多个线程并发访问公共资源时的安全状态是针对多线程并发场景的特性关注的是线程之间的资源竞争可重入侧重描述一个函数本身的设计特性关注的是函数是否能被多个执行流重复进入其适用场景不仅包括多线程还包括信号中断等单进程内的重入场景。一个重要的结论线程安全不一定是可重入的。例如一个函数通过加锁保护共享资源实现了线程安全但如果该函数在持有锁的情况下被重入如信号处理函数调用该函数就会因为锁未释放而导致死锁此时该函数是线程安全的但不可重入。1.3 实战代码线程安全与可重入的对比1线程不安全的函数示例#include iostream #include pthread.h #include unistd.h // 全局变量无锁保护 int g_count 0; // 线程不安全的计数函数 void count_add() { for (int i 0; i 10000; i) { g_count; // 非原子操作多线程并发会导致计数错乱 } } // 线程回调函数 void* thread_func(void* arg) { count_add(); return nullptr; } int main() { pthread_t t1, t2; // 创建两个线程并发执行count_add pthread_create(t1, nullptr, thread_func, nullptr); pthread_create(t2, nullptr, thread_func, nullptr); // 等待线程执行完成 pthread_join(t1, nullptr); pthread_join(t2, nullptr); // 预期结果20000实际结果远小于20000 std::cout g_count g_count std::endl; return 0; }编译运行g -o unsafe unsafe.cpp -lpthread ./unsafe # 输出示例g_count 12345每次运行结果不同问题分析g_count是由加载、加 1、写回三条汇编指令组成的非原子操作两个线程并发执行时会相互覆盖导致计数错乱。2线程安全但不可重入的函数示例#include iostream #include pthread.h #include signal.h #include unistd.h int g_count 0; pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; // 加锁实现线程安全但不可重入 void count_add_safe() { pthread_mutex_lock(mutex); // 加锁 for (int i 0; i 1000; i) { g_count; } // 模拟信号中断触发重入 raise(SIGUSR1); pthread_mutex_unlock(mutex); // 解锁 } // 信号处理函数重入count_add_safe void sig_handler(int sig) { std::cout signal sig caught, reenter count_add_safe std::endl; count_add_safe(); } void* thread_func(void* arg) { count_add_safe(); return nullptr; } int main() { // 注册信号处理函数 signal(SIGUSR1, sig_handler); pthread_t t1; pthread_create(t1, nullptr, thread_func, nullptr); pthread_join(t1, nullptr); std::cout g_count g_count std::endl; return 0; }编译运行g -o reenter reenter.cpp -lpthread ./reenter # 程序死锁无输出问题分析函数count_add_safe通过加锁实现了线程安全但在持有锁的情况下触发了信号信号处理函数又重入了该函数导致二次加锁最终引发死锁。该函数是线程安全但不可重入的典型案例。3可重入函数示例#include iostream #include pthread.h #include unistd.h // 可重入函数仅使用局部变量无全局/静态资源无锁 int count_add_reentrant(int init, int num) { int count init; // 局部变量线程私有 for (int i 0; i num; i) { count; } return count; } // 线程回调函数 void* thread_func(void* arg) { // 每个线程传入不同的初始值结果独立 int res count_add_reentrant(*(int*)arg, 10000); std::cout thread pthread_self() res res std::endl; return nullptr; } int main() { pthread_t t1, t2; int a 0, b 10000; // 创建两个线程并发调用可重入函数 pthread_create(t1, nullptr, thread_func, a); pthread_create(t2, nullptr, thread_func, b); pthread_join(t1, nullptr); pthread_join(t2, nullptr); return 0; }编译运行g -o reentrant reentrant.cpp -lpthread ./reentrant # 输出 # thread 140709260797696 res 10000 # thread 140709252404992 res 20000结果分析可重入函数仅使用局部变量每个线程的调用都是独立的不存在资源竞争既保证了线程安全又支持重入。二、常见锁概念死锁是头号敌人避坑是核心在多线程编程中我们通过锁来保护共享资源实现线程互斥与同步。但锁并不是万能的死锁是锁使用过程中最常见也最致命的问题。除此之外我们还需要了解锁的核心特性和各类锁的设计思想才能做到合理使用锁。2.1 死锁多线程编程的 “幽灵”1死锁的定义死锁是指在一组进程 / 线程中每个进程 / 线程都占有其他进程 / 线程需要的资源且不会主动释放自己占有的资源最终导致所有进程 / 线程都处于永久等待的状态。简单来说死锁就是 “互相拿捏”线程 A 持有锁 1想要获取锁 2线程 B 持有锁 2想要获取锁 1二者都不释放自己的锁最终陷入无限等待。2死锁的四个必要条件死锁的发生必须同时满足四个必要条件缺少任何一个死锁都不会发生。这是我们解决死锁问题的核心依据互斥条件一个资源每次只能被一个执行流使用锁的核心特性不可破坏请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放线程持有已获取的锁同时请求新的锁不剥夺条件一个执行流已获得的资源在未使用完之前不能被强行剥夺操作系统不会主动抢占线程持有的锁循环等待条件若干执行流之间形成头尾相接的循环等待资源的关系线程 A 等线程 B 的锁线程 B 等线程 A 的锁。注意互斥条件是锁的本质我们无法也不需要破坏不剥夺条件由操作系统机制决定也难以破坏。因此解决死锁的核心思路是破坏 “请求与保持条件” 和 “循环等待条件”。3死锁的实战代码示例#include iostream #include pthread.h #include unistd.h // 定义两个互斥锁 pthread_mutex_t mtx1 PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mtx2 PTHREAD_MUTEX_INITIALIZER; // 线程1先加锁1再尝试加锁2 void* thread1(void* arg) { pthread_mutex_lock(mtx1); std::cout thread1: lock mtx1 success std::endl; sleep(1); // 让线程2有时间获取锁2制造死锁条件 pthread_mutex_lock(mtx2); std::cout thread1: lock mtx2 success std::endl; // 解锁死锁后不会执行到这里 pthread_mutex_unlock(mtx2); pthread_mutex_unlock(mtx1); return nullptr; } // 线程2先加锁2再尝试加锁1 void* thread2(void* arg) { pthread_mutex_lock(mtx2); std::cout thread2: lock mtx2 success std::endl; sleep(1); // 让线程1有时间获取锁1制造死锁条件 pthread_mutex_lock(mtx1); std::cout thread2: lock mtx1 success std::endl; // 解锁死锁后不会执行到这里 pthread_mutex_unlock(mtx1); pthread_mutex_unlock(mtx2); return nullptr; } int main() { pthread_t t1, t2; pthread_create(t1, nullptr, thread1, nullptr); pthread_create(t2, nullptr, thread2, nullptr); pthread_join(t1, nullptr); pthread_join(t2, nullptr); // 销毁锁死锁后不会执行到这里 pthread_mutex_destroy(mtx1); pthread_mutex_destroy(mtx2); return 0; }编译运行g -o deadlock deadlock.cpp -lpthread ./deadlock # 输出 # thread1: lock mtx1 success # thread2: lock mtx2 success # 程序卡死无后续输出问题分析线程 1 持有 mtx1等待 mtx2线程 2 持有 mtx2等待 mtx1满足死锁的四个必要条件最终程序死锁。2.2 避免死锁的核心方法基于死锁的四个必要条件我们总结了几种实用、易操作的死锁避免方法这些方法在实际开发中能解决 99% 的死锁问题。1保证加锁顺序一致最常用这是破坏循环等待条件的核心方法所有线程在获取多个锁时都按照固定的顺序加锁。例如上述死锁案例中让线程 1 和线程 2 都先加 mtx1再加 mtx2就不会出现循环等待的情况死锁自然不会发生。修改后的代码示例// 线程2修改加锁顺序先加mtx1再加mtx2 void* thread2(void* arg) { pthread_mutex_lock(mtx1); // 固定顺序先加mtx1 std::cout thread2: lock mtx1 success std::endl; sleep(1); pthread_mutex_lock(mtx2); std::cout thread2: lock mtx2 success std::endl; pthread_mutex_unlock(mtx2); pthread_mutex_unlock(mtx1); return nullptr; }编译运行g -o no_deadlock no_deadlock.cpp -lpthread ./no_deadlock # 输出 # thread1: lock mtx1 success # thread1: lock mtx2 success # thread2: lock mtx1 success # thread2: lock mtx2 success # 程序正常退出结果分析线程 1 先获取 mtx1线程 2 等待 mtx1线程 1 执行完成释放所有锁后线程 2 才能获取 mtx1进而获取 mtx2无死锁。2一次性获取所有需要的锁破坏请求与保持条件让线程在执行任务前一次性获取所有需要的锁如果有任何一个锁获取失败就释放已获取的所有锁重新尝试。这种方法破坏了 “请求与保持条件”因为线程不会持有部分锁并请求其他锁。在 C11 及以上版本中可以使用std::lock函数实现多个锁的一次性获取该函数会保证原子性地获取所有锁避免死锁。代码示例#include iostream #include mutex #include thread #include unistd.h std::mutex mtx1, mtx2; void func1() { // 一次性获取mtx1和mtx2原子操作避免死锁 std::lock(mtx1, mtx2); // 用std::lock_guard管理锁的释放RAII风格 std::lock_guardstd::mutex lock1(mtx1, std::adopt_lock); std::lock_guardstd::mutex lock2(mtx2, std::adopt_lock); std::cout func1: get all locks success std::endl; sleep(1); } void func2() { // 同样一次性获取所有锁顺序无关 std::lock(mtx2, mtx1); std::lock_guardstd::mutex lock2(mtx2, std::adopt_lock); std::lock_guardstd::mutex lock1(mtx1, std::adopt_lock); std::cout func2: get all locks success std::endl; sleep(1); } int main() { std::thread t1(func1); std::thread t2(func2); t1.join(); t2.join(); return 0; }编译运行g -o one_lock one_lock.cpp -lpthread -stdc11 ./one_lock # 输出 # func1: get all locks success # func2: get all locks success # 程序正常退出关键说明std::lock原子性地获取多个互斥锁避免死锁std::adopt_lock表示锁已经被获取std::lock_guard仅负责解锁不负责加锁RAII 风格利用对象的构造和析构自动管理锁的生命周期避免手动解锁遗漏导致的死锁。3给锁添加超时机制让线程在获取锁时设置超时时间如果在指定时间内没有获取到锁就放弃获取并释放已持有的所有锁避免永久等待。在 POSIX 线程中可以使用pthread_mutex_timedlock函数实现带超时的加锁该函数在超时后会返回错误码不会一直阻塞。代码示例#include iostream #include pthread.h #include unistd.h #include time.h pthread_mutex_t mtx1 PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mtx2 PTHREAD_MUTEX_INITIALIZER; void* thread1(void* arg) { pthread_mutex_lock(mtx1); std::cout thread1: lock mtx1 success std::endl; // 设置超时时间5秒后超时 struct timespec ts; clock_gettime(CLOCK_REALTIME, ts); ts.tv_sec 5; // 尝试加锁mtx25秒超时 int ret pthread_mutex_timedlock(mtx2, ts); if (ret 0) { std::cout thread1: lock mtx2 success std::endl; pthread_mutex_unlock(mtx2); } else { std::cout thread1: lock mtx2 timeout, release mtx1 std::endl; } // 释放已持有的锁 pthread_mutex_unlock(mtx1); return nullptr; } void* thread2(void* arg) { pthread_mutex_lock(mtx2); std::cout thread2: lock mtx2 success std::endl; sleep(10); // 持有锁210秒让线程1加锁2超时 pthread_mutex_unlock(mtx2); return nullptr; } int main() { pthread_t t1, t2; pthread_create(t1, nullptr, thread1, nullptr); pthread_create(t2, nullptr, thread2, nullptr); pthread_join(t1, nullptr); pthread_join(t2, nullptr); pthread_mutex_destroy(mtx1); pthread_mutex_destroy(mtx2); return 0; }编译运行g -o timeout_lock timeout_lock.cpp -lpthread ./timeout_lock # 输出 # thread1: lock mtx1 success # thread2: lock mtx2 success # thread1: lock mtx2 timeout, release mtx1 # 程序正常退出结果分析线程 1 获取 mtx1 后尝试获取 mtx2 超时主动释放 mtx1避免了死锁。4避免锁未释放的场景锁的未释放是导致死锁的常见诱因比如程序在持有锁时抛出异常未执行解锁操作手动解锁时遗漏了某些分支的解锁线程在持有锁时退出未释放锁。解决方法使用 RAII 风格的锁管理如 C 的std::lock_guard、std::unique_lock或自定义的 LockGuard利用对象的析构函数自动解锁无论程序正常执行还是抛出异常都能保证锁被释放。自定义 RAII 锁示例基于 POSIX 线程#include pthread.h // 互斥锁封装 class Mutex { public: Mutex() { pthread_mutex_init(_mtx, nullptr); } ~Mutex() { pthread_mutex_destroy(_mtx); } void lock() { pthread_mutex_lock(_mtx); } void unlock() { pthread_mutex_unlock(_mtx); } pthread_mutex_t* get() { return _mtx; } // 禁止拷贝和赋值 Mutex(const Mutex) delete; Mutex operator(const Mutex) delete; private: pthread_mutex_t _mtx; }; // RAII风格的锁守卫 class LockGuard { public: explicit LockGuard(Mutex mtx) : _mtx(mtx) { _mtx.lock(); } ~LockGuard() { _mtx.unlock(); } // 析构自动解锁 // 禁止拷贝和赋值 LockGuard(const LockGuard) delete; LockGuard operator(const LockGuard) delete; private: Mutex _mtx; }; // 使用示例 Mutex mtx; void func() { LockGuard lock(mtx); // 构造加锁 // 业务逻辑即使抛出异常析构也会自动解锁 }关键优势RAII 风格的锁管理从根本上避免了手动解锁的遗漏是多线程编程中推荐的锁使用方式。2.3 其他常见锁概念悲观锁、乐观锁、自旋锁除了互斥锁实际开发中还会接触到悲观锁、乐观锁、自旋锁等概念这些锁并非具体的锁实现而是锁的设计思想适用于不同的业务场景。1悲观锁悲观锁的设计思想是总是假设最坏的情况认为数据一定会被其他线程修改因此在每次访问数据前都先加锁阻止其他线程访问。我们之前使用的互斥锁pthread_mutex_t、std::mutex都是典型的悲观锁。悲观锁的核心是阻塞等待如果一个线程获取锁失败就会被挂起直到锁被释放。适用场景写操作频繁的场景比如多线程对共享资源的修改操作远多于读操作悲观锁能有效避免数据竞争。缺点线程的挂起和唤醒会带来一定的系统开销在高并发场景下可能会影响性能。2乐观锁乐观锁的设计思想是总是假设最好的情况认为数据不会被其他线程修改因此访问数据时不加锁只在更新数据时判断数据是否被修改。乐观锁没有具体的锁实现而是通过版本号机制或CAS 操作实现。版本号机制为数据添加一个版本号每次更新数据时版本号加 1更新前判断版本号是否与获取时一致一致则更新否则重试CAS 操作Compare And Swap原子操作包含三个操作数 —— 内存地址、旧值、新值。当内存地址中的实际值等于旧值时将其修改为新值否则操作失败一般通过自旋重试。适用场景读操作频繁写操作稀少的场景比如缓存、配置信息等乐观锁避免了加锁的开销提升了并发性能。缺点存在ABA 问题数据被修改为 B 后又改回 ACAS 会认为数据未被修改可能导致逻辑错误解决方法添加版本号或时间戳自旋重试会占用 CPU 资源若写操作频繁会导致 CPU 利用率过高。CAS 操作实战代码基于 C11 原子操作#include iostream #include atomic #include thread #include vector std::atomicint g_count(0); // 原子变量支持CAS操作 void count_add() { for (int i 0; i 10000; i) { // CAS自旋更新直到更新成功为止 int old g_count.load(); while (!g_count.compare_exchange_weak(old, old 1)) { // 旧值被修改重新加载 } } } int main() { std::vectorstd::thread threads; // 创建10个线程并发执行 for (int i 0; i 10; i) { threads.emplace_back(count_add); } for (auto t : threads) { t.join(); } // 预期结果100000实际结果准确 std::cout g_count g_count std::endl; return 0; }编译运行g -o cas cas.cpp -lpthread -stdc11 ./cas # 输出g_count 100000关键说明std::atomic是 C11 提供的原子类型其compare_exchange_weak方法实现了 CAS 操作保证了更新的原子性无需加锁即可实现线程安全。3自旋锁自旋锁的设计思想是当线程获取锁失败时不会被挂起而是通过循环不断尝试获取锁直到获取成功。自旋锁的核心是忙等而非阻塞等待适用于锁持有时间极短的场景。因为线程的挂起和唤醒开销远大于自旋的 CPU 开销此时自旋锁的性能更优。与互斥锁的区别互斥锁获取锁失败→线程挂起→CPU 调度其他线程自旋锁获取锁失败→自旋重试→不放弃 CPU。适用场景锁持有时间短、CPU 核心数充足的场景避免单核心下自旋导致其他线程无法执行。缺点若锁持有时间过长自旋会占用大量 CPU 资源导致系统性能下降。注意POSIX 线程中没有直接提供自旋锁的接口但 Linux 内核提供了pthread_spinlock_t实现自旋锁使用方式与互斥锁类似。三、STL、智能指针与线程安全那些容易踩坑的点在 C 多线程编程中除了手动管理共享资源和锁我们还会频繁使用 STL 容器和智能指针。很多开发者会想当然地认为这些标准库组件是线程安全的实则不然 ——STL 和智能指针的线程安全特性有明确的边界超出边界就会引发问题。3.1 STL 容器的线程安全默认非线程安全需手动保护结论先行C 标准库中的 STL 容器默认都不是线程安全的。1为什么 STL 容器设计为非线程安全STL 的设计初衷是极致的性能而加锁保证线程安全会带来显著的性能损耗。此外不同容器的使用场景不同加锁的方式如整锁、细粒度锁也不同标准库无法为所有场景提供通用的加锁方案因此将线程安全的责任交给了开发者。例如对于std::vector的遍历加整锁会导致并发读的性能大幅下降而对于std::unordered_map可以采用桶级锁对每个哈希桶单独加锁提升并发性能标准库无法兼顾所有情况。2STL 容器的有限线程安全保证虽然 STL 容器默认非线程安全但 C11 及以上版本对 STL 容器的线程安全做了有限的保证在以下场景中容器是线程安全的多个线程同时对容器进行读操作无写操作时多个线程并发读 STL 容器是安全的一个线程写多个线程读需手动加锁无安全保证多个线程同时对容器进行写操作必须手动加锁否则会导致容器内部数据结构错乱如链表断裂、哈希桶冲突。3STL 容器的线程安全实战加锁保护代码示例多线程对std::vector进行读写操作通过互斥锁保证线程安全#include iostream #include vector #include pthread.h #include unistd.h #include LockGuard.hpp // 引入之前自定义的RAII锁 std::vectorint g_vec; Mutex g_mtx; // 全局互斥锁 // 写线程向vector中添加元素 void* write_func(void* arg) { for (int i 0; i 1000; i) { LockGuard lock(g_mtx); // RAII加锁 g_vec.push_back(i); } std::cout write thread pthread_self() done std::endl; return nullptr; } // 读线程遍历vector并输出大小 void* read_func(void* arg) { for (int i 0; i 100; i) { LockGuard lock(g_mtx); // RAII加锁 std::cout read thread pthread_self() vec size: g_vec.size() std::endl; sleep(1); } std::cout read thread pthread_self() done std::endl; return nullptr; } int main() { pthread_t w1, w2, r1, r2; // 创建两个写线程两个读线程 pthread_create(w1, nullptr, write_func, nullptr); pthread_create(w2, nullptr, write_func, nullptr); pthread_create(r1, nullptr, read_func, nullptr); pthread_create(r2, nullptr, read_func, nullptr); // 等待所有线程完成 pthread_join(w1, nullptr); pthread_join(w2, nullptr); pthread_join(r1, nullptr); pthread_join(r2, nullptr); // 最终vector大小应为2000 std::cout final vec size: g_vec.size() std::endl; return 0; }关键说明对 STL 容器的所有读写操作都必须加锁保护即使是size()、empty()等看似简单的操作因为这些操作也可能涉及容器内部的全局状态修改非原子操作。3.2 智能指针的线程安全unique_ptr 安全shared_ptr 有限安全C 的智能指针主要包括unique_ptr、shared_ptr和weak_ptr三者的线程安全特性差异较大核心区别在于是否存在共享的状态。1unique_ptr完全线程安全unique_ptr是独占式智能指针其特性是一个unique_ptr对象独占一个资源不支持拷贝仅支 持移动。unique_ptr的所有操作都基于局部对象存储在线程栈中属于线程私有资源不存在多个线程并发访问同一个unique_ptr对象的情况因此unique_ptr在多线程环境下是完全线程安全的。代码示例#include iostream #include memory #include thread void func() { // unique_ptr是局部变量线程私有 std::unique_ptrint p(new int(10)); std::cout thread std::this_thread::get_id() : *p std::endl; } int main() { std::thread t1(func); std::thread t2(func); t1.join(); t2.join(); return 0; }结果分析两个线程的unique_ptr是独立的局部对象无资源竞争线程安全。2shared_ptr有限线程安全引用计数原子操作shared_ptr是共享式智能指针其核心特性是多个shared_ptr对象共享同一个资源通过引用计数管理资源的生命周期。shared_ptr的线程安全特性分为两个层面引用计数的操作是原子的、线程安全的C 标准库规定shared_ptr的引用计数增加copy和减少destruct操作是原子操作由编译器通过 CAS 实现无需开发者手动加锁对shared_ptr对象本身的操作是非线程安全的如果多个线程并发修改同一个shared_ptr对象如reset、assign则需要手动加锁保护否则会导致数据竞争。简单来说多个线程并发拷贝同一个shared_ptr对象共享资源是安全的多个线程并发修改同一个shared_ptr对象是不安全的。3shared_ptr 的线程安全实战示例① 安全场景多线程拷贝 shared_ptr引用计数操作#include iostream #include memory #include thread #include vector // 全局shared_ptr多个线程拷贝 std::shared_ptrint g_sp(new int(10)); void copy_sp() { for (int i 0; i 10000; i) { std::shared_ptrint p g_sp; // 拷贝引用计数原子增加 } } int main() { std::vectorstd::thread threads; for (int i 0; i 10; i) { threads.emplace_back(copy_sp); } for (auto t : threads) { t.join(); } // 引用计数最终为1所有临时对象析构引用计数原子减少 std::cout g_sp use_count: g_sp.use_count() std::endl; return 0; }编译运行g -o shared_safe shared_safe.cpp -lpthread -stdc11 ./shared_safe # 输出g_sp use_count: 1结果分析多线程拷贝g_sp引用计数的增加和减少都是原子操作线程安全。② 不安全场景多线程修改同一个 shared_ptr 对象#include iostream #include memory #include thread #include vector std::shared_ptrint g_sp(new int(10)); // 多线程修改同一个g_sp对象非线程安全 void modify_sp() { for (int i 0; i 1000; i) { g_sp.reset(new int(i)); // 修改g_sp非原子操作 } } int main() { std::vectorstd::thread threads; for (int i 0; i 10; i) { threads.emplace_back(modify_sp); } for (auto t : threads) { t.join(); } std::cout *g_sp *g_sp std::endl; return 0; }问题分析多个线程并发调用g_sp.reset()修改同一个shared_ptr对象reset操作包含多个步骤减少旧引用计数、分配新资源、设置新引用计数非原子操作会导致数据竞争程序可能崩溃或结果错乱。③ 解决方法加锁保护 shared_ptr 对象的修改#include iostream #include memory #include thread #include vector #include mutex std::shared_ptrint g_sp(new int(10)); std::mutex g_mtx; // 互斥锁保护g_sp的修改 void modify_sp_safe() { for (int i 0; i 1000; i) { std::lock_guardstd::mutex lock(g_mtx); // 加锁 g_sp.reset(new int(i)); } } int main() { std::vectorstd::thread threads; for (int i 0; i 10; i) { threads.emplace_back(modify_sp_safe); } for (auto t : threads) { t.join(); } std::cout *g_sp *g_sp std::endl; return 0; }编译运行g -o shared_unsafe_fix shared_unsafe_fix.cpp -lpthread -stdc11 ./shared_unsafe_fix # 输出*g_sp 999结果确定线程安全结果分析通过加锁保护g_sp的修改操作保证了原子性解决了数据竞争问题。4weak_ptr依赖 shared_ptr 的线程安全weak_ptr是为了解决shared_ptr的循环引用问题而设计的它不管理资源的生命周期仅作为shared_ptr的观察者其线程安全特性完全依赖于所指向的shared_ptr对weak_ptr的lock()操作获取shared_ptr是线程安全的因为本质是对shared_ptr引用计数的原子操作对weak_ptr对象本身的修改操作如reset若多个线程并发执行需要手动加锁保护。总结多线程编程的核心是平衡线程安全与并发性能线程锁是保障线程安全的重要工具但锁的使用并非越多越好 —— 过度加锁会导致并发性能下降甚至引发死锁不加锁则会导致数据竞争程序执行错乱。想要掌握多线程编程不仅需要理解线程安全、锁、死锁等核心概念更需要在实际开发中不断实践积累避坑经验。希望本文能帮助你从原理到实战彻底掌握线程安全与线程锁的核心知识让你在多线程编程的世界里游刃有余告别各种坑如果本文对你有帮助欢迎点赞、收藏、关注三连 后续会持续更新多线程编程的进阶内容包括读写锁、条件变量、线程池的深度实战敬请期待