建设网站的公司要什么资质,公司网站更新,沈阳男科医院去哪里,网页设计公司怎么开目录 1.资源共享问题
1.1多线程并发访问
1.2临界区和临界资源
1.3互斥锁
2.多线程抢票
2.1并发抢票
2.2 引发问题
3.线程互斥
3.1互斥锁相关操作
3.1.1互斥锁创建与销毁
3.1.2、加锁操作
3.1.3 解锁操作
3.2.解决抢票问题
3.2.1互斥锁细节
3.3互斥…目录 1.资源共享问题
1.1多线程并发访问
1.2临界区和临界资源
1.3互斥锁
2.多线程抢票
2.1并发抢票
2.2 引发问题
3.线程互斥
3.1互斥锁相关操作
3.1.1互斥锁创建与销毁
3.1.2、加锁操作
3.1.3 解锁操作
3.2.解决抢票问题
3.2.1互斥锁细节
3.3互斥锁原理
3.4多线程封装
3.5互斥锁的封装
3.5.1RAII风格
4.线程安全VS重入
5、常见锁概念
5.1、死锁问题
6.线程同步
6.1同步概念
6.2.同步相关操作
6.2.1条件变量创建与销毁
6.2.2条件等待
6.2.3唤醒线程
6.3同步demo 1.资源共享问题
1.1多线程并发访问 比如存在全局变量 g_val 以及两个线程 thread_A 和 thread_B两个线程同时不断对 g_val 做 减减 -- 操作 注意用户的代码无法直接对内存中的 g_val 做修改需要借助 CPU 如果想要对 g_val 进行修改至少要分为三步
先将 g_val 的值拷贝至寄存器中在 CPU 内部通过运算寄存器完成计算将寄存器中的值拷贝回内存
假设 g_val 初始值为 100如果 thread_A 想要进行 g_val--就必须这样做 也就是说简单的一句 g_val-- 语句实际上至少会被分成 三步
单线程场景下步骤分得再细也没事因为没有其他线程干扰它但我们现在是在 多线程 场景中存在 线程调度问题假设此时 thread_A 在执行完第2步后被强行切走了换成 thread_B 运行 thread_A 的第3步还没有完成内存中 g_val 的值还没有被修改但 thread_A 认为自己已经修改了完成了第2步在线程调度时thread_A 的上下文及相关数据会被保存thread_A 被切走后thread_B 会被即刻调度入场不断执行 g_val-- 操作
thread_B 的运气比较好进行很多次 g_val-- 操作后都没有被切走 当 thread_B 将 g_val 中的值修改为 10 后就被操作系统切走了此时轮到 thread_A 登场thread_A 带着自己的之前的上下文数据继续进行它的未尽事业完成第3步操作当然 thread_B 的上下文数据也会被保存 此时尴尬的事情发生了thread_A 把 g_val 的值改成了 99这对于 thread_B 来说很不公平倘若下次再从内存中读取 g_val时结果为 99自己又得重新进行计算但站在两个线程的角度来说两者都没有错
thread_A: 将自己的上下文恢复后继续执行操作合情合理thread_B: 按照要求不断对 g_val 进行操作也是合情合理 错就错在 thread_A 在错误的时机被切走了保存了老旧的 g_val 值对于 thread_B 来说直接影响就是 g_val 的值飘忽不定
倘若再出现一个线程 thread_C 不断打印 g_val 的值那么将会看到 g_val 值减为 10 后又突然变为 99 的 “灵异现象”
产出结论多线程场景中对全局变量并发访问不是 100% 可靠的
1.2临界区和临界资源 在多线程场景中对于诸如 g_val 这种可以被多线程看到的同一份资源称为 临界资源涉及对 临界资源 进行操作的上下文代码区域称为 临界区 临界资源 本质上就是 多线程共享资源而 临界区 则是 涉及共享资源操作的代码区间
1.3互斥锁 临界资源 要想被安全的访问就得确保 临界资源使用时的安全性
对于 临界资源 访问时的安全问题也可以通过 加锁 来保证实现多线程间的 互斥访问互斥锁 就是解决多线程并发访问问题的手段之一
我们可以 在进入临界区之前加锁出临界区之后解锁 这样可以确保并发访问 临界资源 时的绝对串行化比如之前的 thread_A 和 thread_B 在并发访问 g_val 时如果进行了 加锁在 thread_A 被切走后thread_B 无法对 g_val 进行操作因为此时 锁 被 thread_A 持有thread_B 只能 阻塞式等待锁直到 thread_A 解锁意味着 thread_A 的整个操作都完成了 因此对于 thread_A 来说在 加锁 环境中只要接手了访问临界资源 g_val 的任务要么完成、要么不完成不会出现中间状态像这种不会出现中间状态、结果可预期的特性称为 原子性 说白了 加锁 的本质就是为了实现 原子性
注意
加锁、解锁是比较耗费系统资源的会在一定程序上降低程序的运行速度加锁后的代码是串行化执行的势必会影响多线程场景中的运行速度所以为了尽可能的降低影响加锁粒度要尽可能的细 2.多线程抢票 实践出真知接下来通过代码演示多线程并发访问问题
2.1并发抢票
思路很简单存在 1000 张票和 5 个线程5 个线程同时抢票直到票数为 0程序结束后可以看看每个线程分别抢到了几张票以及最终的票数是否为 0
共识购票需要时间抢票成功后也需要时间这里通过 usleep 函数模拟耗费时间
#include iostream
#include string
#include unistd.h
#include pthread.h
using namespace std;int tickets 1000; // 有 1000 张票void* threadRoutine(void* args)
{int sum 0;const char* name static_castconst char*(args); while(true){// 如果票数 0 才能抢if(tickets 0){usleep(2000); // 耗时 2mssum;--tickets;}elsebreak; // 没有票了usleep(2000); //抢到票后也需要时间处理}cout 线程 name 抢票完毕最终抢到的票数 sum endl;delete name;return nullptr;
}int main()
{pthread_t pt[5];for(int i 0; i 5; i){char* name new char(16);snprintf(name, 16, thread-%d, i);pthread_create(pt i, nullptr, threadRoutine, name);}for(int i 0; i 5; i)pthread_join(pt[i], nullptr);cout 所有线程均已退出剩余票数: tickets endl;return 0;
}最终剩余票数 -1难道 12306 还欠了 1张票这显然是不可能的5 个线程抢到的票数之和为 1001这就更奇怪了总共 1000张票还多出来 1 张 显然多线程并发访问是绝对存在问题的
2.2 引发问题
这其实就是 thread_A 和 thread_B 并发访问 g_val 时遇到的问题举个例子假设 tickets 500thread-0 在抢票准备完成第3步将数据拷贝回内存时被切走了thread-1 抢票后tickets 499轮到 thread-0 回来时它也是把 tickets 修改成了 499这就意味着 thread-0 和 thread-1 之间有一个人白嫖了一张票按理来说 tickets 498 才对 对于 票 这种 临界资源可以通过 加锁 进行保护即实现 线程间的互斥访问确保多线程购票时的 原子性
3 条汇编指令要么不执行要么全部一起执行完 --tickets 本质上是 3 条汇编指令在任意一条执行过程中切走线程都会引发并发访问问题 3.线程互斥
互斥 - 互斥排斥事件 A 与事件 B 不会同时发生
比如 多线程并发抢票场景中可以通过添加 互斥锁 的方式来确保同一张票不会被多个线程同时抢到
3.1互斥锁相关操作
3.1.1互斥锁创建与销毁 互斥锁 同样出自 原生线程库类型为 pthread_mutex_t互斥锁 在创建后需要进行 初始化
#include pthread.hpthread_mutex_t mtx; // 定义一把互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);其中
参数1 pthread_mutex_t* 表示想要初始化的锁这里传的是地址因为需要在初始化函数中对 互斥锁 进行初始化
参数2 const pthread_mutexattr_t* 表示初始化时 互斥锁 的相关属性设置传递 nullptr 使用默认属性
返回值初始化成功返回 0失败返回 error number
互斥锁 是一种向系统申请的资源在 使用完毕后需要销毁
#include pthread.hint pthread_mutex_destroy(pthread_mutex_t *mutex);其中只有一个参数 pthread_mutex_t* 表示想要销毁的 互斥锁
返回值销毁成功返回 0失败返回 error number
以下是创建并销毁一把 互斥锁 的示例代码
#include iostream
#include pthread.h
using namespace std;int main()
{pthread_mutex_t mtx; //定义互斥锁pthread_mutex_init(mtx, nullptr); // 初始化互斥锁// ...pthread_mutex_destroy(mtx); // 销毁互斥锁return 0;
}注意
互斥锁是一种资源一种线程依赖的资源因此 [初始化互斥锁] 操作应该在线程创建之前完成[销毁互斥锁] 操作应该在线程运行结束后执行总结就是 使用前先创建使用后需销毁对于多线程来说应该让他们看到同一把锁否则就没有意义不能重复销毁互斥锁已经销毁的互斥锁不能再使用
使用 pthread_mutex_init 初始化 互斥锁 的方式称为 动态分配需要手动初始化和销毁除此之外还存在 静态分配即在定义 互斥锁 时初始化为 PTHREAD_MUTEX_INITIALIZER
pthread_mutex_t mtx PTHREAD_MUTEX_INITIALIZER;静态分配 的优点在于 无需手动初始化和手动销毁锁的生命周期伴随程序缺点就是定义的 互斥锁 必须为 全局互斥锁 注意 使用静态分配时互斥锁必须定义为全局锁
3.1.2、加锁操作
互斥锁最重要的功能就是 加锁与解锁操作主要使用pthread_mutex_lock 进行加锁
#include pthread.hint pthread_mutex_lock(pthread_mutex_t *mutex);参数 pthread_mutex_t* 表示想要使用哪把互斥锁进行加锁操作
返回值成功返回 0失败返回 error number
使用 pthread_mutex_lock 加锁时可能遇到的情况
当前互斥锁没有被别人持有正常加锁函数返回 0当前互斥锁被别人持有加锁失败当前线程被阻塞执行流被挂起无法向后运行直到获得 [锁资源]
3.1.3 解锁操作 使用 pthread_mutex_unlock 进行 解锁
#include pthread.hint pthread_mutex_unlock(pthread_mutex_t *mutex);参数 pthread_mutex_t* 表示想要对哪把互斥锁进行解锁
返回值解锁成功返回 0失败返回 error number
在 加锁 成功并完成对 临界资源 的访问后就应该进行 解锁将 [锁资源] 让出供其他线程执行流进行 加锁
注意 如果不进行解锁操作会导致后续线程无法申请到 [锁资源] 而永久等待引发 死锁 问题
3.2.解决抢票问题
为了方便所有线程看到同一把 锁可以给线程信息创建一个类 TData其中包括 name 和 pmtx
pmtx 表示指向 互斥锁 的指针 #include iostream
#include string
#include unistd.h
#include pthread.h
using namespace std;int tickets 1000; // 有 1000 张票// 需要定义在 threadRoutine 之前
class TData
{
public:TData(const string name, pthread_mutex_t* pmtx):_name(name), _pmtx(pmtx){}public:string _name;pthread_mutex_t* _pmtx;
};void* threadRoutine(void* args)
{int sum 0;TData* td static_castTData*(args); while(true){// 进入临界区加锁pthread_mutex_lock(td-_pmtx);// 如果票数 0 才能抢if(tickets 0){usleep(2000); // 耗时 2mssum;tickets--;// 出临界区了解锁pthread_mutex_unlock(td-_pmtx);}else{// 如果判断没有票了也应该解锁pthread_mutex_unlock(td-_pmtx);break; // 没有票了}// 抢到票后还有后续动作usleep(2000); //抢到票后也需要时间处理}// 屏幕也是共享资源加锁可以有效防止打印结果错行pthread_mutex_lock(td-_pmtx);cout 线程 td-_name 抢票完毕最终抢到的票数 sum endl;pthread_mutex_unlock(td-_pmtx);delete td;return nullptr;
}int main()
{// 创建一把锁pthread_mutex_t mtx;// 在线程创建前初始化互斥锁pthread_mutex_init(mtx, nullptr);pthread_t pt[5];for(int i 0; i 5; i){char* name new char(16);snprintf(name, 16, thread-%d, i);TData *td new TData(name, mtx);pthread_create(pt i, nullptr, threadRoutine, td);}for(int i 0; i 5; i)pthread_join(pt[i], nullptr);cout 所有线程均已退出剩余票数: tickets endl;// 线程退出后销毁互斥锁pthread_mutex_destroy(mtx);return 0;
}假设某个线程在解锁后没有后续动作那么它会再次加锁继续干自己的事如此重复形成竞争锁该线程独享一段时间的资源 解决方法解锁后让当前线程执行其他动作也可以选择休眠一段时间确保 [锁资源] 能尽可能均匀的分发给其他线程 3.2.1互斥锁细节
多线程加锁互斥中的细节处理才是重头戏
细节1 凡是访问同一个临界资源的线程都要进行加锁保护而且必须加同一把锁这是游戏规则必须遵守
比如在上面的代码中5 个并发线程看到的是同一把 互斥锁只有看到同一把 互斥锁 才能确保线程间 互斥 细节2 每一个线程访问临界区资源之前都要加锁本质上是给临界区加锁
并且建议加锁时粒度要尽可能的细因为加锁后区域的代码是串行化执行的代码量少一些可以提高多线程并发时的效率
细节3 线程在访问临界区前需要先加锁 - 所有线程都要看到同一把锁 - 锁本身也是临界资源 - 锁如何保证自己的安全
加锁 是为了保护 临界资源 的安全但 锁 本身也是 临界资源这就像是一个 先有鸡还是先有蛋的问题锁 的设计者也考虑到了这个问题于是对于 锁 这种 临界资源 进行了特殊化处理加锁 和 解锁 操作都是原子的不存在中间状态也就不需要保护了 细节4 临界区本身是一行代码或者一批代码
线程在执行临界区内的代码时可以被调度吗 调度切换后对于锁及临界资源有影响吗 首先线程在执行临界区内的代码时是允许被调度的比如线程 1 在持有 [锁资源] 后结束运行是完全可行的证明可以被调度其次线程在持有锁的情况下被调度是没有影响的不会扰乱原有的加锁次序
简单举例说明
假设你的学校里有一个 顶级 VIP 自习室一次只允许一个人使用。作为学校里的公共资源这个 顶级 VIP 自习室 开放给所有学生使用
使用规则
一次只允许一个人使用 自习室的门上装有一把锁优先到达自习室的可以获取钥匙并进入自习室 自习室内无限制允许一直自习直到自愿退出退出后需要把钥匙交给下一个想要自习的同学 假设某天早上 6:00 张三就到达了 顶级 VIP 自习室并成功获取钥匙解锁后进入了自习室自习之后陆陆续续有同学来到了 顶级 VIP 自习室 门口因为他们都没有钥匙只能默默等待张三或上一个进入自习室的人交接钥匙。 此时的张三不就是持有 [锁资源]并且在进行 临界资源 访问的 线程执行流 吗其他线程执行流无法进入 临界区只有等待张三 解锁交出 [锁资源] / 钥匙 假如张三此时想上厕所并且不想失去钥匙那么此时他就会带着钥匙去上厕所即便自习室空无一人但其他同学也无法进入自习室 张三上厕所的行为可以看作线程在持有 [锁资源] 的情况下被调度了显然此时对于整体程序是没有影响的因为 锁还是处于 lock 状态其他线程无法进入临界区 假若张三自习够了潇洒出门把钥匙往门上一放正好被李四同学抢到了那么此时 顶级 VIP 自习室 就是属于李四的 交接钥匙的本质是让出 自习室 的访问权这不就是 线程解锁后离开临界区其他线程加锁并进入临界区吗
综上可以借助 张三与顶级 VIP 自习室 的故事理解 线程持有锁时的各种状态
细节5 互斥会给其他线程带来影响
当某个线程持有 [锁资源] 时对于其他线程的有意义的状态
锁被我申请了其他线程无法获取锁被我释放了其他线程可以获取锁
在这两种状态的划分下确保了多线程并发访问时的 原子性
细节6 加锁与解锁配套出现并且这两个对于锁的操作本身就是原子的
至于如何确保 加锁和解锁 时的原子性可以接着往下看
3.3互斥锁原理 在如今大多数 CPU 的体系结构比如 ARM、X86、AMD 等都提供了 swap 或者 exchange 指令这种指令可以把 寄存器 和 内存单元 的数据 直接交换由于这种指令只有一条语句可以保证指令执行时的 原子性 即便是在多处理器环境下总线只有一套访问内存的周期也有先后一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期即 swap 和 exchange 指令在多处理器环境下也是原子的 首先看一段伪汇编代码加锁相关的
本质上就是 pthread_mutex_lock() 函数
lock:movb $0, %alxchgb %al, mutexif(al寄存器里的内容 0){return 0;} else挂起等待;goto lock;其中 movb 表示赋值al 为一个寄存器xchgb 就是支持原子操作的 exchange 交换语句
共识计算机中的硬件如 CPU 中的寄存器只有一份被所有线程共享但其中的内容随线程不同线程的内容可能不同也就是我们常说的上下文数据
寄存器 ! 寄存器中的内容执行流的上下文 当线程 thread_A 首次加锁时整体流程如下
将 0 赋值给 al 寄存器这里假设 mutex 默认值为 1其他不为 0 的整数也行
movb $0, %al将 al 寄存器中的值与 mutex 的值交换原子操作
xchgb %al, mutex判断当前 al 寄存器中的值是否 0
if(al寄存器里的内容 0){return 0;} else挂起等待;此时线程 thread_A 就可以快快乐乐的访问 临界区 代码了如果此时线程 thread_A 被切走了并没有出临界区[锁资源] 也没有释放OS 会保存 thread_A 的上下文数据并让线程 thread_B 入场 thread_B 也是执行 pthread_mutex_lock() 的代码试图进入 临界区
首先将 al 寄存器中的值赋为 0 movb $0, %al其次将 al 寄存器中的值与 mutex 的值交换原子操作 mutex 作为内存中的值被所有线程共享因此 thread_B 看到的 mutex 是被 thread_A 修改后的值 显然此时交换了个寂寞
最后判断 al 寄存器中的值是否 0 if(al寄存器里的内容 0){return 0;
} else挂起等待;此时的 thread_B 因为没有 [锁资源] 而被拒绝进入 临界区不止是 thread_B 后续再多线程除了 thread_A 都无法进入 临界区
不难看出此时 thread_A 的上下文数据中al 1 正是解开 临界区 的 钥匙其他线程是无法获取的因为 钥匙 只能有一份
而汇编代码中 xchgb %al, mutex 的本质就是 加锁当 mutex 不为 0 时表示 钥匙 可用可以进行 加锁并且因为 xchgb %al, mutex 只有一条汇编指令足以确保 加锁 过程是 原子性 的 现在再来看看 解锁 操作吧本质上就是执行 pthread_mutex_unlock() 函数 原理相同
unlock:movb $1, mutex唤醒等待 [锁资源] 的线程;return注意
加锁是一个让不让你通过的策略交换指令 swap 或 exchange 是原子的确保 锁 这个临界资源不会出现问题未获取到 [锁资源] 的线程会被阻塞至 pthread_mutex_lock() 处
3.4多线程封装
目标对 原生线程库 提供的接口进行封装进一步提高对线程相关接口的熟练程度
既然是封装那必然离不开类这里的类成员包括
线程 ID线程名 name线程状态 status线程回调函数 fun_t传递给回调函数的参数 args
#pragma once#include iostream
#include string
#include pthread.henum class Status
{NEW 0, // 新建RUNNING, // 运行中EXIT // 已退出
};// 参数、返回值为 void 的函数类型
typedef void (*func_t)(void*);class Thread
{
private:pthread_t _tid; // 线程 IDstd::string _name; // 线程名Status _status; // 线程状态func_t _func; // 线程回调函数void* args; // 传递给回调函数的参数
};首先完成 构造函数初始化时只需要传递 编号、函数、参数 就行了
Thread(int num 0, func_t func nullptr, void* args nullptr):_tid(0), _status(Status::NEW), _func(func), _args(args)
{// 根据编号写入名字char name[128];snprintf(name, sizeof name, thread-%d, num);_name name;
}其次完成各种获取具体信息的接口
// 获取 ID
pthread_t getTID() const
{return _tid;
}// 获取线程名
std::string getName() const
{return _name;
}// 获取状态
Status getStatus() const
{return _status;
}接下来就是处理 线程启动
// 启动线程
void run()
{int ret pthread_create(_tid, nullptr, runHelper, nullptr /*需要考虑*/);if(ret ! 0){std::cerr create thread fail! std::endl;exit(1); // 创建线程失败直接退出}_status Status::RUNNING; // 更改状态为 运行中
}线程执行的方法依赖于回调函数 runHelper
// 回调方法
void* runHelper(void* args)
{// 很简单回调用户传进来的 func 函数即可_func(_args);
}此时这里出现问题了pthread_create 无法使用 runHelper 进行回调 参数类型不匹配
原因在于类中的函数方法默认有一个隐藏的 this 指针指向当前对象显然此时 tunHelper 中的参数列表无法匹配
解决方法有几种解决方法这里选一个比较简单粗暴的直接把 runHelper 函数定义为 static 静态函数这样他就会失去隐藏的 this 指针 不过此时又出现了一个新问题失去 this 指针后就无法访问类内成员了也就无法进行回调了
有点尴尬不过换个思路既然他想要 this 指针那我们直接利用 pthread_create 的参数4 进行传递就好了实现曲线救国 // 回调方法
static void* runHelper(void* args)
{Thread* myThis static_castThread*(args);// 很简单回调用户传进来的 func 函数即可myThis-_func(myThis-_args);return nullptr;
}// 启动线程
void run()
{int ret pthread_create(_tid, nullptr, runHelper, this);if(ret ! 0){std::cerr create thread fail! std::endl;exit(1); // 创建线程失败直接退出}_status Status::RUNNING; // 更改状态为 运行中
}在最后完成 线程等待
// 线程等待
void join()
{int ret pthread_join(_tid nullptr);if(ret ! 0){std::cerr thread join fail! std::endl;exit(1); // 等待失败直接退出}_status Status::EXIT; // 更改状态为 退出
}现在使用自己封装的 Demo版线程库简单编写多线程程序
注意 需要包含头文件我这里是 Thread.hpp
#include iostream
#include unistd.h
#include Thread.hpp
using namespace std;void threadRoutine(void* args)
{}int main()
{Thread t1(1, threadRoutine, nullptr);cout thread ID: t1.getTID() , thread name: t1.getName() , thread status: (int)t1.getStatus() endl;t1.run();cout thread ID: t1.getTID() , thread name: t1.getName() , thread status: (int)t1.getStatus() endl;t1.join();cout thread ID: t1.getTID() , thread name: t1.getName() , thread status: (int)t1.getStatus() endl;return 0;
}3.5互斥锁的封装
原生线程库 提供的 互斥锁 相关代码比较简单也比较好用但有一个很麻烦的地方就是每次都得手动加锁、解锁如果忘记解锁还会导致其他线程陷入无限阻塞的状态
因此我们对锁进行封装实现一个简单易用的 小组件
封装思路利用创建对象时调用构造函数对象生命周期结束时调用析构函数的特点融入 加锁、解锁 操作即可
非常简单直接创建一个 LockGuard 类
#include iostream
#include unistd.h
#include Thread.hpp
#include LockGuard.hpp
using namespace std;// 创建一把全局锁
pthread_mutex_t mtx;
int tickets 1000; // 有 1000 张票// 自己封装的线程库返回值为 void
void threadRoutine(void *args)
{int sum 0;const char* name static_castconst char*(args);while (true){// 进入临界区加锁{// 自动加锁、解锁LockGuard guard(mtx);// 如果票数 0 才能抢if (tickets 0){usleep(2000); // 耗时 2mssum;tickets--;}elsebreak; // 没有票了}// 抢到票后还有后续动作usleep(2000); // 抢到票后也需要时间处理}// 屏幕也是共享资源加锁可以有效防止打印结果错行{LockGuard guard(mtx);cout 线程 name 抢票完毕最终抢到的票数 sum endl;}
}int main()
{// 在线程创建前初始化互斥锁pthread_mutex_init(mtx, nullptr);// 创建一批线程Thread t1(1, threadRoutine, (void*)thread-1);Thread t2(2, threadRoutine, (void*)thread-2);Thread t3(3, threadRoutine, (void*)thread-3);// 启动t1.run();t2.run();t3.run();// 等待t1.join();t2.join();t3.join();// 线程退出后销毁互斥锁pthread_mutex_destroy(mtx);cout 剩余票数: tickets endl;return 0;
}3.5.1RAII风格 像这种 获取资源即初始化 的风格称为 RAII 风格由 C 之父 本贾尼·斯特劳斯特卢普 提出非常巧妙的运用了 类和对象 的特性实现半自动化操作
4.线程安全VS重入
线程安全多线程并发访问同一段代码时不会出现不同的结果此时就是线程安全的但如果在没有加锁保护的情况下访问全局变量或静态变量导致出现不同的结果此时线程就是不安全的 重入同一个函数被多个线程执行流调用当前一个执行流还没有执行完函数时其他执行流可以进入该函数这种行为称之为 重入在发生重入时函数运行结果不会出现问题称该函数为 可重入函数否则称为 不可重入函数 常见线程不安全的情况 不保护共享变量比如全局变量和静态变量函数的状态随着被调用而导致状态发生变化返回指向静态变量指针的函数调用 线程不安全函数 的函数 常见线程安全的情况 每个线程对全局变量或静态变量只有读取权限而没有写入权限一般来说都是线程安全的类或者接口对于线程来说都是原子操作多个线程之间的切换不会导致执行结果存在二义性 常见不可重入的情况 调用了 malloc / free 函数因为这些都是 C语言 提供的接口通过全局链表进行管理调用了标准 I/O 库函数其中很多实现都是以不可重入的方式来使用全局数据结构可重入函数体内使用了静态的数据结构 常见可重入的情况 不使用全局变量或静态变量不使用 malloc 或 new 开辟空间不调用不可重入函数不返回全局或静态数据所有的数据都由函数调用者提供使用本地数据或者通过制作全局数据的本地拷贝来保护全局数据 重入与线程安全的联系 如果函数是可重入的那么函数就是线程安全的不可重入的函数有可能引发线程安全问题如果一个函数中使用了全局数据那么这个函数既不是线程安全的也不是可重入的 重入与线程安全的区别 可重入函数是线程安全函数的一种线程安全不一定是可重入的反过来可重入函数一定是线程安全的如果对于临界资源的访问加上锁则这个函数是线程安全的但如果这个重入函数中没有被释放会引发死锁因此是不可被重入的
一句话总结是否可重入只是函数的一种特征没有好坏之分但线程不安全是需要规避的
5、常见锁概念 5.1、死锁问题 死锁指在一组进程中的各个线程均占有不会释放的资源但因相互申请被其他线程所占用不会释放的资源处于一种永久等待状态
概念比较绕简单举个例子
两个小朋各持 五毛钱 去商店买东西俩人同时看中了一包 辣条但这包 辣条 售价 一块钱两个小朋友都想买了自己吃但彼此的钱都不够双方互不谦让此时局面就会僵持不下
两个小朋友两个不同的线程 辣条临界资源 售价访问临界资源需要的锁资源数量这里需要两把锁 两个小朋友各自手里的钱一把锁资源 僵持不下的场面形成死锁导致程序无法继续运行
所以死锁就是 多个线程都因锁资源的等待而被同时挂起导致程序陷入 死循环 只有一把锁会造成死锁吗 答案是 会的如果线程 thread_A 申请锁资源访问完临界资源后没有释放会导致 线程 thread_B 无法申请到锁资源同时线程 thread_A 自己也申请不到锁资源了不就是 死锁 吗 死锁 产生的四个必要条件
互斥一个资源每次只能被一个执行流使用请求与保持一个执行流因请求资源而阻塞时对已获得的资源保持不释放环路等待若干执行流之间形成一种首尾相接的循环等待资源关系不剥夺条件不能强行剥夺其他线程的资源
只有四个条件都满足了才会引发 死锁 问题
如何避免 死锁 问题 核心思想破坏四个必要条件的其中一个或多个
方法1不加锁
不加锁的本质是不保证 互斥即破坏条件1
方法2尝试主动释放锁
比如进入 临界区 访问 临界资源需要两把锁thread_A 和 thread_B 各自持有一把锁并且都在尝试申请第二把锁但如果此时 thread_A 放弃申请主动把锁释放这样就能打破 死锁 的局面主打的就是一个牺牲自己
可以借助 pthread_mutex_trylock 函数实现这种方案
#include pthread.hint pthread_mutex_trylock(pthread_mutex_t *mutex);这个函数就是尝试申请锁如果长时间申请不到锁就会把自己当前持有的锁释放然后放弃加锁给其他想要加锁的线程一个机会
方法3按照顺序申请锁
按照顺序申请锁 - 按照顺序释放锁 - 就不会出现环路等待的情况
方法4控制线程统一释放锁
首先要明白锁不一定要由申请锁的线程释放其他线程也可以释放锁
比如在下面这个程序中主线程就释放了次线程申请的锁打破了 死锁 的局面 #include iostream
#include pthread.h
#include unistd.husing namespace std;// 全局互斥锁无需手动初始化和销毁
pthread_mutex_t mtx PTHREAD_MUTEX_INITIALIZER;void* threadRoutine(void* args)
{cout 我是次线程我开始运行了 endl;// 申请锁pthread_mutex_lock(mtx);cout 我是次线程我申请到了一把锁 endl;// 在不释放锁的情况下再次申请锁陷入 死锁 状态pthread_mutex_lock(mtx);cout 我是次线程我又再次申请到了一把锁 endl;pthread_mutex_unlock(mtx);return nullptr;
}int main()
{pthread_t t;pthread_create(t, nullptr, threadRoutine, nullptr);// 等待次线程先跑sleep(3);// 主线程帮忙释放锁pthread_mutex_unlock(mtx);cout 我是主线程我已经帮次线程释放了一把锁 endl;// 等待次线程后续动作sleep(3);pthread_join(t, nullptr);cout 线程等待成功 endl;return 0;
}因此我们可以设计一个 控制线程专门掌管所有的锁资源如果识别到发生了 死锁 问题就释放所有的锁让线程重新竞争
注意 规定只有申请锁的人才能释放锁规定可以不遵守但最好遵守
死锁 一般比较少见因为这是因代码编写失误而引发的问题
常见的避免 死锁 问题的算法死锁检测算法、银行家算法
6.线程同步
6.1同步概念
同步在保证数据安全的前提下让线程能够按照某种特定的顺序访问临界资源从而有效避免 饥饿问题
至于该如何正确理解 饥饿问题需要再次请张三出场
话说张三在早上 6:00 抢到了自习室的钥匙并开开心心的进入了自习室自习
此时自习室外人声鼎沸显然有很多人都在等待张三交出钥匙但张三不急慢悠悠的自习到了中午 12:00此时张三有些饿了想出去吃个饭吃饭就意味着张三需要把钥匙归还这是规定
张三刚把钥匙放到门上扭头就发现了大批的同学正在等待钥匙张三心想要是我就这样把钥匙归还了那等我吃完饭回来岂不是也需要等待
于是法外狂徒张三决定放弃吃饭强忍着饥饿再次拿起钥匙进入了自习室自习刚进入自习室没几分钟肚子就饿的咕咕叫于是张三就又想出去吃饭刚出门归还了钥匙扭头看见大批同学就感觉很亏一咬牙就又拿起钥匙进入了自习室就这样张三反复横跳直到下午 6:00 都还没吃上午饭不仅自己没吃上午饭、没好好自习还导致其他同学无法自习
张三错了吗张三没错十分符合自习室的规定只是 不合理
因为张三这种不合理的行为导致 自习室 资源被浪费了在外等待的同学也失去了自习陷入 饥饿状态活生生被张三 “饿惨了”
为此校方更新了 自习室 的规则
所有自习完的同学在归还钥匙之后不能立即再次申请 在外面等待钥匙的同学必须排队遵守规则 规则更新之后就不会出现这种 饥饿问题 了所以解决 饥饿问题 的关键是在安全的规则下使多线程访问资源具有一定的顺序性
即通过 线程同步 解决 饥饿问题
原生线程库 中提供了 条件变量 这种方式来实现 线程同步 逻辑链通过条件变量 - 实现线程同步 - 解决饥饿问题 条件变量当一个线程互斥的访问某个变量时它可能发现在其他线程改变状态之前什么也做不了
比如当一个线程访问队列时发现队列为空它只能等待直到其他线程往队列中添加数据此时就可以考虑使用 条件变量
条件变量的本质就是 衡量访问资源的状态
竞态条件因为时序问题而导致程序出现异常
可以把 条件变量 看作一个结构体其中包含一个 队列 结构用来存储正在排队等候的线程信息当条件满足时就会取 队头 线程进行操作操作完成后重新进入 队尾 队列是保证顺序性的重要工具 6.2.同步相关操作
6.2.1条件变量创建与销毁 作为出自 原生线程库 的 条件变量使用接口与 互斥锁 风格差不多比如 条件变量 的类型为 pthread_cond_t同样在创建后需要初始化
#include pthread.hpthread_cond_t cond; // 定义一个条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);参数1 pthread_cond_t* 表示想要初始化的条件变量
参数2 const pthread_condattr_t* 表示初始化时的相关属性设置为 nullptr 表示使用默认属性
返回值成功返回 0失败返回 error number
条件变量 在使用结束后需要销毁 #include pthread.hint pthread_cond_destroy(pthread_cond_t *cond);pthread_cond_t* 表示想要销毁的条件变量
返回值成功返回 0失败返回 error number
注同互斥锁一样条件变量支持静态分配即在创建全局条件变量时定义为 PTHREAD_COND_INITIALIZER表示自动初始化、自动销毁
pthread_cond_t cond PTHREAD_COND_INITIALIZER;注意 这种定义方式只支持全局条件变量
6.2.2条件等待 原生线程库 中提供了 pthread_cond_wait 函数用于等待
#include pthread.hint pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);参数1 pthread_cond_t* 想要加入等待的条件变量
参数2 pthread_mutex_t* 互斥锁用于辅助条件变量
返回值成功返回 0失败返回 error number
参数2值得详细说一说首先要明白 条件变量是需要配合互斥锁使用的需要在获取 [锁资源] 之后在通过条件变量判断条件是否满足
传递互斥锁的理由
条件变量也是临界资源需要保护当条件不满足时没有被唤醒当前持有锁的线程就会被挂起其他线程还在等待锁资源呢为了避免死锁问题条件变量需要具备自动释放锁的能力
当某个线程被唤醒时条件变量释放锁该线程会获取锁资源并进入 条件等待 状态
6.2.3唤醒线程 条件变量 中的线程是需要被唤醒的否则它也不知道何时对 队头线程 进行判断可以使用 pthread_cond_signal 函数进行唤醒
#include pthread.hint pthread_cond_signal(pthread_cond_t *cond);pthread_cond_t 表示想要从哪个条件变量中唤醒线程
返回值成功返回 0失败返回 error number
注意 使用 pthread_cond_signal 一次只会唤醒一个线程即队头线程
如果想唤醒全部线程可以使用 pthread_cond_broadcast
#include pthread.hint pthread_cond_broadcast(pthread_cond_t *cond);6.3同步demo
接下来简单使用一下 线程同步 相关接口
目标创建 5 个次线程等待条件满足主线程负责唤醒
这里演示 单个唤醒 与 广播 两种方式先来看看 单个唤醒 相关代码 #include iostream
#include pthread.h
#include unistd.husing namespace std;// 互斥锁和条件变量都定义为自动初始化和释放
pthread_mutex_t mtx PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond PTHREAD_COND_INITIALIZER;const int num 5; // 创建五个线程void* Active(void* args)
{const char* name static_castconst char*(args);while(true){// 加锁pthread_mutex_lock(mtx);// 等待条件满足pthread_cond_wait(cond, mtx);cout \t线程 name 正在运行 endl;// 解锁pthread_mutex_unlock(mtx);}delete[] name;return nullptr;
}int main()
{pthread_t pt[num];for(int i 0; i num; i){char* name new char[32];snprintf(name, 32, thread-%d, i);pthread_create(pt i, nullptr, Active, name);}// 等待所有次线程就位sleep(3);// 主线程唤醒次线程while(true){cout Main thread wake up Other thread! endl;pthread_cond_signal(cond); // 单个唤醒sleep(1);}for(int i 0; i num; i)pthread_join(pt[i], nullptr);return 0;
}可以看到在 单个唤醒 模式下一次只会有一个线程苏醒并且得益于 条件变量线程苏醒的顺序都是一样的 可以将唤醒方式换成 广播
// ......
pthread_cond_broadcast(cond); // 广播
// ......互斥锁条件变量 可以实现 生产者消费者模型关于 生产者消费者的实现 与 条件变量 的更多细节将会在下一篇文章中揭晓