Linux线程同步与互斥(六):线程安全、可重入与死锁
为什么有些函数不能在多线程环境下使用为什么加了锁的程序还是可能崩溃什么是死锁STL容器线程安全吗一、线程安全和重入话题1.1 线程安全定义多个线程同时访问一个函数或对象不会出现不确定的结果即使没有额外的同步机制或者有正确的同步也能正确执行。通俗理解你的代码不会因为多线程调度而产生“数据损坏”或“逻辑错误”。例子全局变量int counter两个线程同时counter→ 不安全。如果对counter加锁保护 → 安全。翻译成大白话多个线程一起跑一起读一起写一起改结果永远正确不会出现负数、错乱、覆盖、脏数据 → 就是线程安全1.2 可重入定义函数可以被多个执行流同时进入而不会产生数据错乱。这里的“执行流”可以是多个线程也可以是同一个线程在执行过程中被信号处理函数打断后再次进入该函数。通俗理解函数即使被“重入”比如刚执行到一半又再次被调用也能正确运行。典型场景信号处理函数中调用的函数必须是可重入的因为信号可能在主程序的任意位置触发导致同一个函数被重复进入。小白话函数跑到一半被 “中途插队” 再进一次两种重入场景多线程重入两个线程同时进同一个函数信号打断重入一个线程被信号中断又进一次可重入函数的特点不使用全局变量、静态变量不调用 malloc/free不调用不可重入函数所有变量都是局部变量栈上1.3 什么时候线程不安全只要出现下面任意一种一定不安全多个线程访问共享资源全局变量 / 静态变量又读又写没有加锁保护满足这 3 条 线程不安全1.4 总结重点不要被上面的绕口令式的话语吓唬住你只需要指定其实对应概念说的都是一回事最简单的 3 句话可重入函数一定是线程安全的线程安全函数不一定是可重入的使用了全局变量但加了锁 → 线程安全但不可重入为什么加锁的函数是线程安全但不可重入void func() { lock(); // 加锁 ... // 中途被信号打断 unlock(); // 没执行到 }信号来了 → 又进一次 func ()→ 再次 lock ()→ 锁已经被持有 →死锁所以✅ 线程安全多线程用没问题❌ 不可重入信号 / 递归重入会死锁二、常见锁概念2.1 死锁两个或多个线程互相持有对方需要的资源并且都不释放导致所有线程永久阻塞。你拿着我要的我拿着你要的互相不释放 →永远卡住假设现在的线程A、线程B必须同时拥有锁1和锁2才能进行后续资源的访问// 线程 A // 线程 B pthread_mutex_lock(mutex1); pthread_mutex_lock(mutex2); pthread_mutex_lock(mutex2); pthread_mutex_lock(mutex1);申请一把锁式原子的但是申请两把锁就不一定了。造成的结果是2.2 死锁的四个必要条件互斥条件 一个资源每次只能被一个执行流使用请求与保持条件一个执行流因请求资源而阻塞时对已经获得的资源保持不放不剥夺条件一个执行流已经获得的资源在未使用完之前不得强行剥夺循环等待条件若干执行流之间形成一种头尾相接的循环等待资源的关系条件说明互斥资源一次只能被一个线程占用请求与保持线程持有资源的同时请求其他资源不剥夺线程不释放已占有的资源循环等待存在等待环路A等BB等A2.3 避免死锁破坏“请求与保持”一次性申请所有资源std::lock或同时加多把锁。破坏“循环等待”所有线程按固定顺序加锁例如总是先锁 mutex1 再锁 mutex2。破坏“不剥夺”使用pthread_mutex_trylock失败时释放已有的锁。使用超时机制pthread_mutex_timedlock。示例固定顺序加锁// 线程 A 和 B 都先锁 mutex1再锁 mutex2 pthread_mutex_lock(mutex1); pthread_mutex_lock(mutex2);示例使用std::lock一次锁多把锁C11std::lock(mutex1, mutex2); // 不会死锁 std::lock_guardstd::mutex lock1(mutex1, std::adopt_lock); std::lock_guardstd::mutex lock2(mutex2, std::adopt_lock);2.4 常见锁概念简介锁类型特点适用场景互斥锁独占锁阻塞等待保护普通临界区自旋锁忙等待不释放CPU临界区极短避免上下文切换读写锁多读单写读多写少乐观锁不加锁更新前检查版本数据库、并发控制悲观锁每次访问都加锁冲突概率高CASCompare And Swap原子比较并交换无锁编程基础悲观锁 vs 乐观锁悲观锁假设冲突会发生提前加锁乐观锁假设冲突很少更新时检测失败则重试。避免死锁算法死锁检测算法、银行家算法三、STL、智能指针和线程安全3.1 STL容器是否线程安全不是。STL容器的设计目标是极致性能没有内部同步机制。多线程环境下多个线程同时读取同一个容器是安全的。只要有一个线程写就必须由用户加锁保护。std::vectorint vec; // 多线程 push_back 需加锁3.2 智能指针是否线程安全std::unique_ptr不涉及共享完全在线程栈上安全。std::shared_ptr引用计数是原子操作所以多个线程同时拷贝、析构shared_ptr是安全的计数不会出错。但是指向的对象本身不是线程安全的如果多个线程通过shared_ptr修改同一个对象仍需要加锁std::shared_ptrint sp std::make_sharedint(10); // 多个线程拷贝 sp 或让 sp 析构安全 // 但 *sp 20; 不安全需保护