当前位置: 首页 > news >正文

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 思想的体现


总结

  难死了我靠,下篇讲线程同步!!!

相关文章:

  • vue3 + echarts(5.6.0)实现渐变漏斗图
  • Rocketmq broker 是主从架构还是集群架构,可以故障自动转移吗
  • Android Edge-to-Edge
  • C++ Pimpl(Pointer to Implementation)设计思想
  • 香港科技大学广州香港科技大学硕博士研究生学位项目宣讲会(智能制造硕博士物理学硕士)—深圳大学专场
  • TuyaOpen横空出世!涂鸦智能如何用开源框架重构AIoT开发范式?
  • PostgreSQL简介安装
  • 分频电路设计
  • WIFI信号状态信息 CSI 深度学习之数据集
  • taro 小程序 CoverImage Image src无法显示图片的问题
  • 顶级流媒体服务商 Spotify 2025.04 故障复盘报告,吃他人的堑长自己的智
  • Python + moviepy:根据图片或数据高效生成视频全流程详解
  • mac .zshrc:1: command not found: 0 解决方案
  • Java操作Elasticsearch 之 [Java High Level REST Clientedit]
  • MongoDB 学习(三)Redis 与 MongoDB 的区别
  • 外部因素导致的 ADC误差来源分析
  • python训练 60天挑战-day31
  • cmw500测gps抗干扰能力测试方法及注意事项
  • 高阶数据结构——AVL树的实现(详细解答)
  • [自动化集成] 使用明道云上传附件并在Python后端处理Excel的完整流程
  • 一箭六星!力箭一号遥七运载火箭发射成功
  • 斗鱼一季度直播收入降近三成,语音社交服务推高广告等收入,称将持续打击涉赌行为
  • 内塔尼亚胡称将控制“整个加沙”,英、法、加威胁对以“制裁”
  • 中方对美俄领导人就俄乌冲突进行通话有何评论?外交部回应
  • 春决火爆的背后,PEL如何做大这块电竞蛋糕
  • 法国参议院调查委员会公布雀巢“巴黎水”丑闻调查报告