前言
马上要竣事了!!!
我们在学习 多线程 的时候,一定会遇到 并发访问 的题目,最直观的感受就是每次运行得出的结果值大概率不一致,这种实验结果不一致的现象黑白常致命,因为它具有随机性,即结果大概是对的,也大概是错的,无法可靠的完成任务,类似物理学神兽 薛定谔的猫
一、资源共享题目
多线程并发访问
好比存在全局变量 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% 可靠的
临界区与临界资源
在多线程场景中,对于诸如 g_val 这种可以被多线程看到的同一份资源称为 临界资源,涉及对 临界资源 进行操作的上下文代码区域称为 临界区
临界资源 本质上就是 多线程共享资源,而 临界区 则是 涉及共享资源操作的代码区间
“锁”概念引入
临界资源 要想被安全的访问,就得确保 临界资源 使用时的安全性
举个例子:公共厕所是共享的,但卫生间只能供一人使用,为了确保如厕时的安全性,就必要给每个卫生间都加上一道门,而且加上一把锁
对于 临界资源 访问时的安全题目,也可以通过 加锁 来保证,实现多线程间的 互斥访问,互斥锁 就是办理多线程并发访问题目的手段之一
我们可以 在进入临界区之前加锁,出临界区之后解锁, 这样可以确保并发访问 临界资源 时的绝对串行化,好比之前的 thread_A 和 thread_B 在并发访问 g_val 时,如果进行了 加锁,在 thread_A 被切走后,thread_B 无法对 g_val 进行操作,因为此时 锁 被 thread_A 持有,thread_B 只能 阻塞式等待锁,直到 thread_A 解锁(意味着 thread_A 的整个操作都完成了)
因此,对于 thread_A 来说,在 加锁 环境中,只要接办了访问临界资源 g_val 的任务,要么完成、要么不完成,不会出现中间状态,像这种不会出现中间状态、结果可预期的特性称为 原子性
说白了 加锁 的本质就是为了实现 原子性
留意:
- 加锁、解锁是比较耗费系统资源的,会在一定步调上低沉步调的运行速率
- 加锁后的代码是串行化实验的,势必会影响多线程场景中的运行速率
- 所以为了尽大概的低沉影响,加锁粒度要尽大概的细
二、多线程抢票
实践出真知,如今我们通过代码来演示多线程并发访问题目
并发抢票
思路很简单:存在 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_cast<const char*>(args);
-
- while(true)
- {
- // 如果票数 > 0 才能抢
- if(tickets > 0)
- {
- usleep(2000); // 耗时 2ms
- sum++;
- --tickets;
- }
- else
- break; // 没有票了
- 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;
- }
复制代码 理想状态下,最终票数为 0,5 个线程抢到的票数之和为 1000,但实际并非如此
最终剩余票数 -2,岂非 12306 还欠了 2 张票?这显然是不大概的,5 个线程抢到的票数之和为 1015,这就更奇怪了,总共 1000 张票还多出来 20 张?
显然多线程并发访问是绝对存在题目的
引发题目
这实在就是 thread_A 和 thread_B 并发访问 g_val 时遇到的题目,举个例子:假设 tickets = 500,thread-0 在抢票,准备完成第3步,将数据拷贝回内存时被切走了,thread-1 抢票后,tickets = 499;轮到 thread-0 返来时,它也是把 tickets 修改成了 499,这就意味着 thread-0 和 thread-1 之间有一个人白嫖了一张票(按理来说 tickets = 498 才对)
要是现实生存中也能那么好就好了!
对于 票 这种 临界资源,可以通过 加锁 进行掩护,即实现 线程间的互斥访问,确保多线程购票时的 原子性
3 条汇编指令要么不实验,要么全部一起实验完
–tickets 本质上是 3 条汇编指令,在任意一条实验过程中切走线程都会引发并发访问题目
三、线程互斥
互斥 -> 互斥排斥:变乱 A 与变乱 B 不会同时发生
好比 多线程并发抢票场景 中可以通过添加 互斥锁 的方式,来确保同一张票不会被多个线程同时抢到
互斥锁相关操作
互斥锁创建与销毁
互斥锁 同样出自 原生线程库,类型为 pthread_mutex_t,互斥锁 在创建后必要进行 初始化
- #include <pthread.h>
- pthread_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.h>
- int 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;
复制代码 静态分配 的长处在于 无需手动初始化和手动销毁,锁的生命周期伴随步调,缺点就是界说的 互斥锁 必须为 全局互斥锁
分配方式操作适用场景动态分配手动初始化/销毁局部锁/全局锁静态分配主动初始化/销毁全局锁 留意: 使用静态分配时,互斥锁必须界说为全局锁
加锁操作
互斥锁 最重要的功能就是 加锁与解锁 操作,主要使用 pthread_mutex_lock 进行 加锁
- #include <pthread.h>
- int pthread_mutex_lock(pthread_mutex_t *mutex);
复制代码 参数 pthread_mutex_t* 表示想要使用哪把互斥锁进行加锁操作
返回值:成功返回 0,失败返回 error number
使用 pthread_mutex_lock 加锁时大概遇到的情况:
- 当前互斥锁没有被别人持有,正常加锁,函数返回 0
- 当前互斥锁被别人持有,加锁失败,当火线程被阻塞(实验流被挂起),无法向后运行,直到获得 [锁资源]
解锁操作
使用 pthread_mutex_unlock 进行 解锁
- #include <pthread.h>
- int pthread_mutex_unlock(pthread_mutex_t *mutex);
复制代码 参数 pthread_mutex_t* 表示想要对哪把互斥锁进行解锁
返回值:解锁成功返回 0,失败返回 error number
在 加锁 成功并完成对 临界资源 的访问后,就应该进行 解锁,将 [锁资源] 让出,供其他线程(实验流)进行 加锁
留意: 如果不进行解锁操作,会导致后续线程无法申请到 [锁资源] 而永久等待,引发 死锁 题目
办理抢票题目
为了方便全部线程看到同一把 锁,可以给线程信息创建一个类 TData,此中包罗 name 和 pmtx
pmtx 表示指向 互斥锁 的指针
- // 需要定义在 threadRoutine 之前
- class TData
- {
- public:
- TData(const string &name, pthread_mutex_t* pmtx)
- :_name(name), _pmtx(pmtx)
- {}
- public:
- string _name;
- pthread_mutex_t* _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_cast<TData*>(args); while(true) { // 进入临界区,加锁 pthread_mutex_lock(td->_pmtx); // 如果票数 > 0 才能抢 if(tickets > 0) { usleep(2000); // 耗时 2ms sum++; 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;}
复制代码 此时无论运行多少次步调,结果都没有题目:最终的剩余票数都是 0,而且全部线程抢到的票数之和为 1000

假设某个线程在解锁后,没有后续动作,那么它会再次加锁,继承干本身的事,如此重复形成竞争锁,该线程独享一段时间的资源
办理方法:解锁后让当火线程实验其他动作,也可以选择休眠一段时间,确保 [锁资源] 能尽大概均匀的分发给其他线程
互斥锁细节
多线程加锁互斥中的细节处理才是重头戏
细节1: 凡是访问同一个临界资源的线程,都要进行加锁掩护,而且必须加同一把锁,这是游戏规则,必须服从
好比在上面的代码中,5 个并发线程看到的是同一把 互斥锁,只有看到同一把 互斥锁 才能确保线程间 互斥
细节2: 每一个线程访问临界区资源之前都要加锁,本质上是给临界区加锁
而且建议加锁时,粒度要尽大概的细,因为加锁后区域的代码是串行化实验的,代码量少一些可以进步多线程并发时的效率
细节3: 线程在访问临界区前,必要先加锁 -> 全部线程都要看到同一把锁 -> 锁本身也是临界资源 -> 锁怎样保证本身的安全?
加锁 是为了掩护 临界资源 的安全,但 锁 本身也是 临界资源,这就像是一个 先有鸡照旧先有蛋的题目,锁 的操持者也思量到了这个题目,于是对于 锁 这种 临界资源 进行了特殊化处理:加锁 和 解锁 操作都是原子的,不存在中间状态,也就不必要掩护了
细节4: 临界区本身是一行代码,或者一批代码
- 线程在实验临界区内的代码时可以被调度吗?
- 调度切换后,对于锁及临界资源有影响吗?
起首,线程在实验临界区内的代码时,是允许被调度的,好比线程 1 在持有 [锁资源] 后竣事运行,是完全可行的(证明可以被调度);其次,线程在持有锁的情况下被调度是没有影响的,不会扰乱原有的加锁次序
我来举个例子
假设你的学校里有一个 顶级 VIP 自习室,一次只允许一个人使用。作为学校里的公共资源,这个 顶级 VIP 自习室 开放给全部门生使用
使用规则如下:
- 一次只允许一个人使用
- 自习室的门上装有一把锁,优先到达自习室的可以获取钥匙并进入自习室
- 自习室内无限制,允许一直自习,直到志愿退出,退出后必要把钥匙交给下一个想要自习的同学
假设某天早上 6:00 张三就到达了 顶级 VIP 自习室,并成功获取钥匙,解锁后进入了自习室自习;之后陆连续续有同学来到了 顶级 VIP 自习室 门口,因为他们都没有钥匙,只能默默等待张三或上一个进入自习室的人交接钥匙
此时的张三不就是持有 [锁资源],而且在进行 临界资源 访问的 线程(实验流) 吗?其他线程(实验流)无法进入 临界区,只有等待张三 解锁(交出 [锁资源] / 钥匙)
假如张三此时想上厕所,而且不想失去钥匙,那么此时他就会带着钥匙去上厕所,即便自习室空无一人,但其他同学也无法进入自习室!
张三上厕所的行为可以看作线程在持有 [锁资源] 的情况下被调度了,显然此时对于整体步调是没有影响的,因为 锁照旧处于 lock 状态,其他线程无法进入临界区
假若张三自习够了,洒脱出门,把钥匙往门上一放,恰恰被李四同学抢到了,那么此时 顶级 VIP 自习室 就是属于李四的
交接钥匙的本质是让出 自习室 的访问权,这不就是 线程解锁后脱离临界区,其他线程加锁并进入临界区吗
综上可以借助 张三与顶级 VIP 自习室 的故事理解 线程持有锁时的各种状态
细节5: 互斥会给其他线程带来影响
当某个线程持有 [锁资源] 时,对于其他线程的故意义的状态:
- 锁被我申请了(其他线程无法获取)
- 锁被我开释了(其他线程可以获取锁)
在这两种状态的划分下,确保了多线程并发访问时的 原子性
细节6: 加锁与解锁配套出现,而且这两个对于锁的操作本身就是原子的
互斥锁的原理
在如今,大多数 CPU 的体系布局(好比 ARM、X86、AMD 等)都提供了 swap 或者 exchange 指令,这种指令可以把 寄存器 和 内存单元 的数据 直接交换,由于这种指令只有一条语句,可以保证指令实验时的 原子性
即便是在多处理器环境下(总线只有一套),访问内存的周期也有先后,一个处理器上的交换指令实验时另一个处理器的交换指令只能等待总线周期,即 swap 和 exchange 指令在多处理器环境下也是原子的
起首看一段伪汇编代码(加锁相关的)
本质上就是 pthread_mutex_lock() 函数
- lock:
- movb $0, %al
- xchgb %al, mutex
- if(al寄存器里的内容 > 0){
- return 0;
- } else
- 挂起等待;
- goto lock;
复制代码 此中 movb 表示赋值,al 为一个寄存器,xchgb 就是支持原子操作的 exchange 交换语句
共识:计算机中的硬件,如 CPU 中的寄存器只有一份,被全部线程共享,但此中的内容随线程,不同线程的内容大概不同,也就是我们常说的上下文数据
寄存器 != 寄存器中的内容(实验流的上下文)
当线程 thread_A 首次加锁时,整体流程如下:
将 0 赋值给 al 寄存器,这里假设 mutex 默认值为 1(其他不为 0 的整数也行)
将 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
其次将 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
复制代码 让 thread_A 登场,并进行 解锁
将 mutex 中的值赋为 1
既然 thread_A 都走到了 解锁 这一步,证明它已经不必要再访问 临界资源 了,可以让其他线程去访问,也就是 叫醒其他等待 [锁资源] 的线程,然后 return 0 走出 临界区
如今 [锁资源] 跑到 thread_B 手里了,并没有新增或丢失,如此重复,就是 加锁 / 解锁 的原理
至于各种被线程实验某条汇编指令时被切出的情况,都可以不会影响整体 加锁 情况
留意:
- 加锁是一个让不让你通过的策略
- 交换指令 swap 或 exchange 是原子的,确保 锁 这个临界资源不会出现题目
- 未获取到 [锁资源] 的线程会被阻塞至 pthread_mutex_lock() 处
多线程封装
如今 互斥 相关内容已经学习的差不多了,可以着手编写一个小组件:Demo版线程库
目标:对 原生线程库 提供的接口进行封装,进一步进步对线程相关接口的熟练程度
既然是封装,那我们肯定离不开类,如今我们来看一下必要哪些类成员:
- 线程 ID
- 线程名 name
- 线程状态 status
- 线程回调函数 fun_t
- 传递给回调函数的参数 args
创建头文件,并编写代码
大体框架如下:
- #pragma once
- #include <iostream>
- #include <string>
- #include <pthread.h>
- enum class Status
- {
- NEW = 0, // 新建
- RUNNING, // 运行中
- EXIT // 已退出
- };
- // 参数、返回值为 void 的函数类型
- typedef void (*func_t)(void*);
- class Thread
- {
- private:
- pthread_t _tid; // 线程 ID
- std::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 指针,指向当前对象,显然此时 runHelper 中的参数列表无法匹配
办理方法:有几种办理方法,这里选一个比较简单粗暴的,直接把 runHelper 函数界说为 static 静态函数,这样他就会失去隐藏的 this 指针
不外此时又出现了一个新题目:失去 this 指针后就无法访问类内成员了,也就无法进行回调了!
有点尴尬,不外换个思路,既然他想要 this 指针,那我们直接使用 pthread_create 的参数4 进行传递就好了,实现曲线救国
- // 回调方法
- static void* runHelper(void* args)
- {
- Thread* myThis = static_cast<Thread*>(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;
- }
复制代码 运行结果如下,可以看出线程的状态从 0 至 2,即 创建 -> 运行 -> 退出
足以证明我们本身封装的 Demo版线程库 没啥大题目
互斥锁的封装
原生线程库 提供的 互斥锁 相关代码比较简单,也比较好用,但有一个很贫苦的地方:就是每次都得手动加锁、解锁,如果忘记解锁,还会导致其他线程陷入无限阻塞的状态
因此我们对锁进行封装,实现一个简单易用的 小组件
封装思路:使用创建对象时调用构造函数,对象生命周期竣事时调用析构函数的特点,融入 加锁、解锁 操作即可
非常简单,直接创建一个 LockGuard 类
- #pragma once
- #include <pthread.h>
- class LockGuard
- {
- public:
- LockGuard(pthread_mutex_t*pmtx)
- :_pmtx(pmtx)
- {
- // 加锁
- pthread_mutex_lock(_pmtx);
- }
- ~LockGuard()
- {
- // 解锁
- pthread_mutex_unlock(_pmtx);
- }
- private:
- pthread_mutex_t* _pmtx;
- };
复制代码 如今把 Demo版线程库 和 Demo版互斥锁 融入 多线程抢票 步调中,可以看到此时代码变得非常优雅
- #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_cast<const char*>(args);
- while (true)
- {
- // 进入临界区,加锁
- {
- // 自动加锁、解锁
- LockGuard guard(&mtx);
- // 如果票数 > 0 才能抢
- if (tickets > 0)
- {
- usleep(2000); // 耗时 2ms
- sum++;
- tickets--;
- }
- else
- break; // 没有票了
- }
- // 抢到票后还有后续动作
- 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;
- }
复制代码
这实在也是一种 RAII 头脑的体现
总结
难死了我靠,下篇讲线程同步!!!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |