Linux线程互斥与同步(上)(29)
文章目录
- 前言
- 一、资源共享问题
- 多线程并发访问
- 临界区与临界资源
- “锁”概念引入
- 二、多线程抢票
- 并发抢票
- 引发问题
- 三、线程互斥
- 互斥锁相关操作
- 解决抢票问题
- 互斥锁的原理
- 多线程封装
- 互斥锁的封装
- 总结
前言
马上要结束了!!!
我们在学习 多线程 的时候,一定会遇到 并发访问 的问题,最直观的感受就是每次运行得出的结果值大概率不一致,这种执行结果不一致的现象是非常致命,因为它具有随机性,即结果可能是对的,也可能是错的,无法可靠的完成任务,类似物理学神兽 薛定谔的猫
一、资源共享问题
多线程并发访问
比如存在全局变量 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); // 耗时 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;
}
理想状态下,最终票数为 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); // 耗时 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;
}
此时无论运行多少次程序,结果都没有问题:最终的剩余票数都是 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, %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
让 thread_A 登场,并进行 解锁
将 mutex 中的值赋为 1
movb $1, mutex
既然 thread_A 都走到了 解锁 这一步,证明它已经不需要再访问 临界资源 了,可以让其他线程去访问,也就是 唤醒其他等待 [锁资源] 的线程,然后 return 0 走出 临界区
唤醒等待 [锁资源] 的线程;
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; // 线程 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 指针,指向当前对象,显然此时 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); // 耗时 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;
}
这其实也是一种 RAII 思想的体现
总结
难死了我靠,下篇讲线程同步!!!
相关文章:
Linux线程互斥与同步(上)(29)
文章目录 前言一、资源共享问题多线程并发访问临界区与临界资源“锁”概念引入 二、多线程抢票并发抢票引发问题 三、线程互斥互斥锁相关操作解决抢票问题互斥锁的原理多线程封装互斥锁的封装 总结 前言 马上要结束了!!! 我们在学习 多线…...
深入解析 hping3网络探测与测试利器
一、什么是 hping3? 体量轻巧:安装包仅约 255 KB。协议多样:支持 TCP、UDP、ICMP、RAW IP 四种模式。灵活定制:可设置任意报文头、分片、Payload 长度;还支持伪造源地址、随机目标等高级操作。脚本化:集成…...
SPA模式下的es6如何加快宿主页的显示速度
SPA的模式下,宿主页是首先加载的页面,会需要一些主要的组件,如element-plus,easyui,devextreme,ant-design等,这些组件及其依赖组件,文件多,代码量大,可能导致…...
环境配置!
1.下载openEuler虚拟机和rocky虚拟机 下载好后,ping一下看一下手动配置的网络ok不,再把复杂密码改成自己能记住的简单密码 2.安装软件 下载yum源 也可以用阿里云的yum源 把里面的:%d全删了,然后把 #generic-repos is licensed …...
【VS Code】Qt程序的调试与性能分析
要对 Qt 程序进行性能分析和调试,尤其是使用像 Valgrind、Perf 或 GDB 这类工具时,通常需要结合开发环境(如 VS Code)与相关插件或命令行工具。 以下是一些常用的方法和步骤: 1. VS Code 调试 Qt 程序 所需配置&…...
记录学习的第三十六天
很久没写过博客了,今天又开始了。 今天很不错,了解了查分数组的实质。 还是做了一道滑动窗口的题,我什么时候才能刷完滑动窗口啊。...
ANSI V 级对夹球阀控制阀:高性价比零泄漏流体控制新选择-耀圣
ANSI V 级对夹球阀控制阀:高性价比零泄漏流体控制新选择 在化工、食品、给排水等工业领域,流体控制的精准性与密封性直接关乎生产安全与效率。ANSI V 级对夹球阀控制阀凭借零泄漏密封性能(ANSI VI 级标准)、紧凑的对夹式结构、亲…...
pcdn核心要素
开展PCDN业务最核心的是明确业务定位、保障网络与硬件基础、确保合规运营,并选择合适的盈利模式。以下是具体要点: 1. 明确业务定位与目标 内容类型适配:PCDN适合高并发、大流量的内容分发场景,如视频直播、点播、大文件下载等。…...
数据分析_主播考核指标体系搭建
作为一名合格的数据分析师,要同时具备逻辑框架搭建能力以及解决实际问题的经验。通过指标量化问题、监控业务健康度并驱动决策。以下是我搭建抖音电商主播考核指标体系时的一些经验,希望对大家有些帮助。 搭建主播能力考核指标体系需要结合直播业务的核心…...
联合索引失效情况分析
一.模拟表结构: 背景: MySQL版本——8.0.37 表结构DDL: CREATE TABLE unite_index_table (id bigint NOT NULL AUTO_INCREMENT COMMENT 主键,clomn_first varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMEN…...
ZYNQ Cache一致性问题解析与实战:从原理到创新优化
一、Cache一致性:多核系统的"记忆迷宫" 1.1 为什么需要关注Cache一致性? 在Zynq-7000系列SoC的双核ARM Cortex-A9架构中,每个CPU核心拥有32KB L1数据Cache和512KB共享L2 Cache。当两个核心同时操作共享内存时,可能会出现: #mermaid-svg-RD2USaYdR7mMPPIA {fon…...
vtkPiecewiseFunction
1. 定义分段函数映射。 2.允许添加控制点,并允许用户控制控制点之间的功能。 3.基于锐度和中点参数,在控制点之间使用分段hermite曲线。 4.锐度为0产生分段线性函数,锐度为1产生分段常数函数。 5.中点是曲线达到Y中值的控制点之间的归一化距离…...
HarmonyOS NEXT~鸿蒙系统与mPaaS三方框架集成指南
HarmonyOS NEXT~鸿蒙系统与mPaaS三方框架集成指南 1. 概述 1.1 鸿蒙系统简介 鸿蒙系统(HarmonyOS)是华为开发的分布式操作系统,具备以下核心特性: 分布式架构:支持跨设备无缝协同微内核设计:提高安全性和性能一次开…...
【老马】流程引擎(Process Engine)概览
前言 大家好,我是老马。 最近想设计一款审批系统,于是了解一下关于流程引擎的知识。 下面是一些的流程引擎相关资料。 工作流引擎系列 工作流引擎-00-流程引擎概览 工作流引擎-01-Activiti 是领先的轻量级、以 Java 为中心的开源 BPMN 引擎&#x…...
基于ROS2/Gazebo的室内送餐机器人系统开发实战教程
1. 系统架构设计 1.1 功能需求分析 #mermaid-svg-Yht1n03rcf5MP4du {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-Yht1n03rcf5MP4du .error-icon{fill:#552222;}#mermaid-svg-Yht1n03rcf5MP4du .error-text{fill:…...
msq基础
一、检索数据 SELECT语句 1.检索单个列 SELECT prod_name FROM products 上述语句用SELECT语句从products表中检索一个名prod_name的列,所需列名在SELECT关键字之后给出,FROM关键字指出从其中检索数据的表名 (返回数据的顺序可能是数据…...
威纶通触摸屏IP地址设定步骤及程序下载指南
在使用威纶通触摸屏时,正确设定IP地址以及完成程序下载是确保其正常运行和实现功能的关键步骤。本文将详细介绍威纶通触摸屏IP地址设定步骤及程序下载的方法。 一、IP地址设定步骤 (一)前期准备 确保威纶通触摸屏已经通电并启动࿰…...
全排列问题深度解析:为何无需index参数且循环从i=0开始?
文章目录 问题背景一、为何回溯函数不需要 index 参数?1. 全排列问题的核心特性2. index 的作用与局限性3. 正确设计:用 used[] 替代 index 二、为何循环从 i0 开始而非 index?1. 排列问题的顺序敏感性2. 对比组合问题的循环设计3. 关键区别总…...
计算机网络通信技术与协议(七)———关于ACL的详细解释
今日学习状态: 关于ACL,我们在之前的博文中有简要的提及到,今天我们将ACL作为一个专题进行讲解: 目录 ACL成立背景: ACL(Access Control List,访问控制列表): 五元组…...
《算法笔记》11.8小节——动态规划专题->总结 问题 D: Coincidence
题目描述 Find a longest common subsequence of two strings. 输入 First and second line of each input case contain two strings of lowercase character a…z. There are no spaces before, inside or after the strings. Lengths of strings do not exceed 100. 输出…...
power BI 倒计时+插件HTML Content,实现更新倒计时看板!
直接拿去玩吧,花了我两个小时。 搜了b站和百度都没找到像样的,就决定自己干一个了。 先看效果: 起个度量值,然后去power bi 插件那边搜索html Content,把这个放进html content插件的字段values即可。 HTML倒计时每周…...
镜像管理(2)Dockerfile总结
一、docker镜像构建方法 commoit :使用 docker commit 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑 箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根 本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具…...
【Tools】neovim操作指南
Neovim 中最常见、最实用的操作, 主要针对C 开发需求: 🚀 基础操作 操作快捷键说明保存:w 或 ZZ保存当前文件退出:q 或 ZQ退出当前窗口保存并退出:wq 或 ZZ保存并退出强制退出:q!不保存直接退出撤销u撤销上一步重做<C-r>重做撤销搜索/xxx向下搜索…...
docker 安装 Nacos
下载镜像 docker pull nacos/nacos-server创建本地目录 mkdir -p /home/nacos/conf /home/nacos/logs运行镜像 docker run -d -p 8848:8848 -e MODEstandalone -e PREFER_HOST_MODEhostname -v /home/nacos/init.d/custom.properties:/home/nacos/init.d/custom.properties …...
【嵌入式】【ESP32】ADF框架
推荐阅读: [005] [ESP32开发笔记] ADF基本框架...
Redisson分布式集合原理及应用
Redisson是一个用于Redis的Java客户端,它简化了复杂的数据结构和分布式服务的使用。 适用场景对比 数据结构适用场景优点RList消息队列、任务队列、历史记录分布式共享、阻塞操作、分页查询RMap缓存、配置中心、键值关联数据支持键值对、分布式事务、TTLRSet去重集…...
一种新兴的网络安全技术:XDR(Extended Detection and Response,扩展检测与响应)(Grok3 DeepSearch模式下回答)
直接回答 XDR(扩展检测与响应)是一种网络安全技术,整合多层数据(如端点、网络、云)以检测和响应威胁。研究表明,它通过AI和自动化提高安全团队效率,减少数据泄露成本。存在原生XDR(…...
使用 Qt Designer 开发
Qt Designer 是属于 Qt Creator 的 一个功能而已,Qt Designer 也叫 UI 设计师或者 UI 设计器,这都是指的同一 个东西而已。 一、在ui文件添加一个按钮 1、新建一个项目 项目名为 02_designer_example 构建系统可选择 CMake , qmake, Qbs 对…...
第7天-Python+PyEcharts实现股票分时图实战教程
分时图是金融领域常用的可视化形式,能够清晰展示价格随时间变化的趋势。本教程将手把手教你用PyEcharts库实现专业级分时图效果。 一、环境准备 bash 复制 下载 pip install pyecharts # 核心可视化库 pip install pandas # 数据处理支持 二、基础分时图实现 1. 模拟…...
Zenmap代理情况下无法扫描ip
原因是开了代理会报错 error “only ethernet devices can be used for raw scans on Windows” 在扫描参数后加 -sT -Pn,但会导致结果太多 例如:nmap -sT -T4 -A -v -Pn 10.44.2.0/24 如果你只是想找没人用的IP,你不需要搞复杂的原始层扫描&…...
JAVA打飞机游戏设计与实现(论文+源代码)【源码+文档+部署】
1 绪论 1.1 手机软件现状 在信息社会中,手机及其他无线设备越来越多的走进普通百姓的工作和生活,随着信息网络化的不断进展,手机及其他无线设备上网络势在必行。但是传统手机存在以下弊端: 1. 传统手机出厂时均由硬件厂商固化…...
C++学习:六个月从基础到就业——多线程编程:std::thread基础
C学习:六个月从基础到就业——多线程编程:std::thread基础 本文是我C学习之旅系列的第五十四篇技术文章,也是第四阶段"并发与高级主题"的第一篇,介绍C11引入的多线程编程基础知识。查看完整系列目录了解更多内容。 引言…...
深入理解指针(一)
1.内存和地址 2.指针变量和地址 3.指针变量类型的意义 4.指针运算 1. 内存和地址 1.1 内存 在讲内存和地址之前,为了大家更好的理解举了这么个例子: 假如有一栋教学楼,刚好你今天在这栋楼的某一个课室上课,已知这栋楼有50个…...
添加currentSchema后,该模式下表报不存在(APP)
文章目录 环境文档用途详细信息相关文档 环境 系统平台:Linux x86-64 Red Hat Enterprise Linux 7 版本:4.5.7 文档用途 解决程序URL添加currentSchema后,访问该模式下的表,报错信息com.highgo.jdbc.util.PSQLException: ERROR…...
Python数据整合与转换全攻略
在大数据时代,企业平均使用16个不同数据源,但数据利用率不足30%。数据整合与转换能力已成为数据工程师的核心竞争力。本文将通过电商订单数据整合实战,系统讲解Python数据整合与转换的核心技术栈。 一、数据整合的三大挑战与应对策略 1. 数…...
ArcGIS操作16:添加经纬网
1、单击视图 > 数据框属性 2、单击格网选项卡 > 新建格网按钮 3、创建经纬网 4、经纬网标注间隔需要自己多次尝试,选择一个合适的值,这里江苏省选择50 5、继续设置合适的参数 6、点击应用,预览是否合适(不合适再新建一个经…...
BioID技术:探索蛋白质相互作用的新方法
在细胞的复杂环境中,蛋白质并非孤立地执行其功能,而是通过与其他蛋白质相互协作来完成各种生物学过程。理解蛋白质相互作用网络对于揭示细胞机制和疾病发生发展具有至关重要的意义。近年来,一种名为BioID(Biotin Identification&a…...
Java 大视界——Java大数据在智慧交通智能停车诱导系统中的数据融合与实时更新
智慧交通的快速发展对城市停车资源的高效利用提出了更高要求,而智能停车诱导系统作为缓解“停车难”问题的核心手段,亟需解决多源数据融合、实时状态更新及高并发访问等挑战。Java凭借其稳定的大数据生态、卓越的实时计算能力及跨平台兼容性,…...
【Redisson】快速实现分布式锁
大家好,我是jstart千语。之前给大家分享过使用redis的set nx ex命令实现分布式锁。但手动的实现方式不仅麻烦,而且不好管理。实现出来的锁也是不可重入的,不可重试的。那么在要求比较高的系统中,就不太适用了。虽然说重入问题可以…...
内核常见面试问题汇总
1、Linux 中主要有哪几种内核锁?它们各自的特点和适用场景是什么? 自旋锁 自旋锁是一种忙等待锁,当一个线程试图获取一个被占用的自旋锁时,他会一直循环在那里,不断地检查锁是否可用,而不会进入睡眠状态。 自旋锁的优点这是在锁被持有的时间很短的情况下,性能非常高,…...
laravel中如何使用Validator::make定义一个变量是 ,必传的,json格式字符串
在 Laravel 中,使用 Validator::make 定义一个变量为必传且为JSON 格式字符串时,可以通过以下方式实现: use Illuminate\Support\Facades\Validator;$validator Validator::make($request->all(), [your_field > required|json, // 必…...
【全解析】EN18031标准下的NMM网络监控机制
上一篇文章我们了解了RLM机制如何为设备抵御DoS攻击保驾护航,今天我们将目光转向 EN18031 标准中的另一个重要防线——NMM(Network Monitoring Mechanism)网络监控机制。 NMM - 1规定,如果设备是网络设备,应提供网络监…...
浏览器开发随笔
浏览器多进程架构(Chrome) ----------------------------- | Browser Process | |-----------------------------| | UI 线程、主控、导航、安全策略 | -----------------------------| | |↓ ↓ ↓ -------…...
漏洞类型与攻击技术
1.1 SQL注入 1.1.1 SQL注入简介与原理 SQL注入是通过用户输入的数据中插入恶意SQL代码,绕过应用程序对数据库的合法操作,进而窃取、篡改或删除数据的攻击方式。核心原理是应用程序未对用户输入进行严格过滤,导致攻击者可以操控SQL语句逻辑。 1.1.2 联合查询注入与报…...
day018-磁盘管理-案例
文章目录 1. 磁盘分区1.1 手动磁盘分区1.2 重装系统,保留分区1.2.1 选择从光盘启动1.2.2 保留系统盘分区1.2.3 挂载数据盘 2. 物理服务器使用流程3. swap3.1 增加swap3.2 关闭swap 4. 故障案例(红帽类系统)4.1 root密码忘记,重新设…...
spark调度系统核心组件SparkContext、DAGSchedul、TaskScheduler介绍
目录 1. SparkContext2.DAGScheduler3. TaskScheduler4. 协作关系Spark调度系统的核心组件主要有SparkContext、DAGScheduler和TaskScheduler SparkContext介绍 1. SparkContext 1、资源申请: SparkContext是Spark应用程序与集群管理器(如Standalone模式下的Master、Yarn模…...
python数据可视化第三章数值计算基础
numpy库 数组创建 import numpy as np #创建n维数组array a np.array([1, 2, 3]) b np.array([4, 5, 6]) #可以直接运算 print(a 1) # [2 3 4] print(ab) # [5 7 9] #数组的维度:ndim print(a.ndim) #1 #数组的形状:shape print(a.shape) #(3,) 一维…...
std::chrono类的简单使用实例及分析
author: hjjdebug date: 2025年 05月 20日 星期二 14:36:17 CST descrip: std::chrono类的简单使用实例及分析 文章目录 1.实例代码:2. 代码分析:2.1 auto t1 std::chrono::high_resolution_clock::now();2.1.1 什么是 system_clock2.1.2 什么是 chrono::time_point?2.1.3 什…...
MongoDB 学习(三)Redis 与 MongoDB 的区别
目录 一、NoSQL 数据库与关系型数据库的优缺点二、Redis 与 MongoDB 的区别 MongoDB 和 Redis 都是 NoSQL 数据库,采用 结构型数据存储,而非 MySQL、Oracle 等则属于传统的 关系型数据库。 一、NoSQL 数据库与关系型数据库的优缺点 关系型数据库&#…...
Java双指针法:原地移除数组元素
Java双指针法:原地移除数组元素 代码解析关键点示例特点 代码解析 class Solution {public int removeElement(int[] nums, int val) {int cur 0; // 初始化一个指针 cur,表示新数组的当前写入位置for (int i 0; i < nums.length; i) { // 遍历原数…...