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

【Linux 系统】互斥与同步

文章目录

  • 1. 互斥
    • 1.1 认识多线程的风险
    • 1.2 认识互斥量
    • 1.3 互斥锁原理
    • 1.4 死锁问题
  • 2. 同步
    • 2.1 认识条件变量
    • 2.2 条件变量小结

1. 互斥

在大部分情况下,线程所使用的数据都是局部变量。变量的地址空间都是在自己维护的栈区中的,这种情况下,变量属于单个线程,其它线程无法获取这个变量。但是,有些时候,很多变量需要在多个线程之间共享,这样的变量我们称之为共享变量,这样我们就可以通过数据的共享,完成一种线程之间的通信。

但是:多线程并发进行操作的同时会给变量带来一些问题!

1.1 认识多线程的风险

来看如下的一个例子:

#define NUM 5int tickets = 1000; // 全局变量,用多线程模拟抢票class threadData
{
public:threadData(int number){_threadname = "thread-" + std::to_string(number);}public:std::string _threadname;
};void *getTicket(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->_threadname.c_str();while (true){if (tickets > 0){usleep(1000);printf("who=%s, get a ticket: %d\n", name, tickets); // ?tickets--;}else{break;}}usleep(13); // 模拟抢完票后的其它方式return nullptr;
}int main()
{std::vector<pthread_t> tids;std::vector<threadData *> thread_datas;for (int i = 1; i <= NUM; i++){pthread_t tid;threadData *td = new threadData(i); // 传参传入pthread_create(&tid, nullptr, getTicket, td);tids.push_back(tid);        // 保存线程idthread_datas.push_back(td); // 保存new的指针}for (auto thread : tids) // 线程等待{pthread_join(thread, nullptr);}for (auto td : thread_datas) // 销毁空间{delete td;}return 0;
}
  • 结果

    在这里插入图片描述
    我们发现这就是多线程下没有保护的情况:共享数据不一致的情况。我们真实的情况是不允许票数能够到达负数的,我们也不允许两个客户抢完票之后是同一个票数,所以我们需要一种办法能够解决这样的问题。

  • 为什么会有这样的差错呢?

    实际上,我们通过一条语句 --tickets 来显示我们模拟抢票的过程,但是 CPU 在执行这样的指令的时候,并不是通过一条一条的语句进行执行的:

    1. 取值到寄存器
    2. 运算计算结果放到寄存器
    3. 将值写回到寄存器

    例如:当线程A执行完步骤1的时候(假设线程的共享变量的值为1000)刚好要执行步骤2的时候,突然另外一个线程B也来了,这个时候线程B读到内存中的值仍然是1000,但是在线程B来之前线程A已经读到了1000,还没来得及修改。这样就导致可能两个线程通过一条语句的时候结果是同一个结果。发生负数的时候也是同理的,这是和线程的时机有关系的。

1.2 认识互斥量

为了解决上面的问题,我们的解决方案就是:在任何时候只允许一个线程访问共享变量

  • 这就是互斥。

我们先来认识接口:

  1. 初始化锁/销毁锁

    在这里插入图片描述
    上面提供了两种方式:一种是局部的互斥锁的初始化和销毁方式;另外一种是全局的互斥锁的初始化。(自动销毁)

  2. 加锁/解锁

    在这里插入图片描述
    lock:是获取互斥锁,如果没有获取到,该线程就在这里阻塞

    trylock:是获取互斥锁,如果没有获取到,该线程就在这里直接返回 -1。

    unlock:对获取到互斥锁的线程解锁,将互斥锁归还。

demo:

int tickets = 1000; // 全局变量,用多线程模拟抢票class threadData
{
public:threadData(int number, pthread_mutex_t* mutex):_mutex(mutex){_threadname = "thread-" + std::to_string(number);}public:std::string _threadname;pthread_mutex_t *_mutex;
};void *getTicket(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->_threadname.c_str();while (true){pthread_mutex_lock(td->_mutex);if (tickets > 0){usleep(1000);printf("who=%s, get a ticket: %d\n", name, tickets);tickets--;}else{pthread_mutex_unlock(td->_mutex);break;}pthread_mutex_unlock(td->_mutex);}usleep(13); // 模拟抢完票后的其它方式return nullptr;
}int main()
{std::vector<pthread_t> tids;std::vector<threadData *> thread_datas;pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);for (int i = 1; i <= NUM; i++){pthread_t tid;threadData *td = new threadData(i, &mutex); // 传参传入pthread_create(&tid, nullptr, getTicket, td);tids.push_back(tid);        // 保存线程idthread_datas.push_back(td); // 保存new的指针}for (auto thread : tids) // 线程等待{pthread_join(thread, nullptr);}for (auto td : thread_datas) // 销毁空间{delete td;}pthread_mutex_destroy(&mutex);return 0;
}
  • 结果:

    在这里插入图片描述

    注意:这里有一个问题,我们能够保证一个数据的安全性了,但是好像只有一个线程去完成这个抢票的工作。造成了其它线程饥饿问题!每一个线程对于锁的竞争力度是不同的,可能一个进程结束了都是一个线程在占用一个锁。

    • 加锁需要加锁在临界区。(需要访问临界资源的代码区域,我们称其为临界区)且我们需要控制加锁时机,在一些场景中,我们具体是最好不让加锁和解锁在循环内的,因为加锁和解锁是有时间开销的。
    • 加锁之后一定需要解锁,不然可能或造成死锁问题。上面我们的 if...else 是两个语句块,这两个语句块只实现其一来执行,所以我们需要在两个语句块中解锁,不然会造成资源浪费/死锁……

关于加锁和解锁:

  1. 加锁的本质:用时间换取数据安全。
  2. 加锁的表现:线程对于临界区的代码执行时串行的。
  3. 加锁的原则:尽量保证临界区的代码较为合适。太少了会造成线程切换的开销(这个时候可以使用自旋锁);太多了就会造成并发度不高。

1.3 互斥锁原理

我们下面给出互斥的伪汇编代码

lock:movb $0, %alxchgb %al, mutexif(%al > 0){return 0;}else{// 挂起等待}goto lock;
unlock:movb $1, mutex//唤醒挂起等待的线程return 0;
  • 解释

    我们认为在汇编层面上一条汇编语句就是原子的。互斥锁的本质就是一个内存中的变量。

    我们在申请锁的时候:

    1. 先把自己的寄存器中的值修改为0。
    2. 再交换内存中 mutex 的值和寄存器中的值
    3. 最后判断寄存器中的值是否非0。如果是,那么就是申请锁资源成功,那么就可以直接返回;否则,申请失败,则挂起等待。

    我们在释放锁的时候:

    1. 直接修改内存中 mutex 的值为1。

    申请锁和释放锁互相配合。申请锁时将寄存器中的值修改为0,保证了当前线程的锁资源申请不会受到自己的上下文影响并且保证了操作的原子性

    从上面的伪代码我们能看出来:申请锁的本质就是将内存中的数据存储到自己线程的上下文中。这样这个锁资源就变成了这个线程所私有的了。所以当一个线程在临界区发生了线程的切换,其它的线程也无法成功的申请到锁资源,因为锁资源已经被该线程带到了上下文中

1.4 死锁问题

  • 死锁

    死锁是多线程或者多进程中编程中的一种情况,其中一组进程或者线程每一个都在等待其它进程/线程释放资源,或者等待其它线程执行操作,而这些被等待的进程/线程也同样的在等待其它的线程释放资源。结果就是:没有一个进程/线程能够向前推进代码。整个系统陷入停滞。

    例如:

    现有线程A,线程B,现有锁a,和锁b。现在线程A、B想要向下执行代码需要两把锁才能向下执行。但是现在发生死锁的情形:线程A申请到了锁a;线程B申请到了锁b。但是两者都需要共同地获取两把锁才能向下执行,这个时候,两个线程就都会阻塞挂起了。

根据上面的一个示例,我们总结发生死锁的必要条件

  1. 互斥条件:一个资源每次只能被一个执行流使用(前提

  2. 请求与保持条件:一个执行流因为请求资源而被阻塞时,对已经获取的资源不进行释放。(原则

  3. 不剥夺条件:一个执行流已获得资源,在未被使用之前不能被强行剥夺/释放(原则

  4. 循环等待条件:若干个执行流之间形成一种头尾相连的循环等待资源就绪的关系(重要条件

也就是意味着:如果上面任意一个条件不满足,那么就不会发生死锁问题。

  • 避免死锁的方式

    1. 破坏死锁的四个必要条件

    2. 加锁顺序一致性

    3. 避免加锁未释放的场景

    4. 资源一次性分配

  • 避免死锁的算法:

    1. 死锁检测算法。
    2. 银行家算法:系统在分配资源之前先检查这次分配是否可能导致系统进入不安全状态,如果可能导致,就不分配这个资源。

2. 同步

回到我们之前的一个问题:“每一个线程对于锁的竞争力度是不同的,可能一个进程结束了都是一个线程在占用一个锁”。这样的场面我们应该怎么解决呢?
这就需要用到同步了。

纯互斥的场景之下,如果资源分配不够合理,那么就会很容易发生线程饥饿的问题。(注意:这里并不是说有互斥就一定会饥饿,最主要的还是看场景)所以,我们想要让获取锁的线程能够以一定的顺序来获取资源,这就是同步问题。

2.1 认识条件变量

所谓条件变量,举一个例子我们现在需要在一个浴室队伍中排队洗澡,这个浴室每次都只能进入一个人。所以我们每一个人都需要在浴室外边排队。当有一个新人来的时候,他会看一下浴室的门是否开着,然后就进入到队伍中进行排队;当一个洗浴完成的人出来之后还想要洗澡,这个时候就需要到队伍的最末尾了。

  1. 初始化和销毁条件变量

    在这里插入图片描述
    这个的接口设计和 mutex 互斥量是相同的。

  2. 条件等待

    在这里插入图片描述
    这里需要传入一个条件变量互斥量

    • 那么我们在等待之前需要先加锁吗?

      这里一定要考虑一个细节:我们为什么要让线程进行等待?不就是因为条件没有就绪吗?所以当在进入条件变量中进行等待的时候一定是需要进行加锁的(在条件变量等待之前)因为我们访问临界资源本身就是不安全的,而是否在条件变量下等待一般就是依据临界资源是否就绪。所以我们必须要加锁。

    • 为什么要传入一个互斥量呢?

      如果加了锁之后,那么持有锁的线程不就在条件变量下等待了吗?后续的线程怎么获取锁?

      这就需要考虑到环境变量的等待设计了。传入一个互斥量的作用:能够让在该条件变量下等待的线程能够释放申请到的锁资源。这样在条件变量下等待的线程就不会因为加锁了之后就会影响其它线程获取锁资源。

  3. 唤醒等待的线程

    在这里插入图片描述

    一般我们可以在临界资源就绪/条件满足的时候就可以唤醒条件变量下等待的线程了。

    • 问题:那么对于一个线程申请到锁资源的线程来说,如果他现在要唤醒其它线程他需要先唤醒还是先释放锁资源呢

      一定是先释放锁资源。这样就避免了唤醒其它线程之后,这些线程仍然获取不到锁资源而阻塞。因为在条件变量下等待的线程,被唤醒之后一定会先去申请所资源的。

demo:

int cnt = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void *Count(void * args)
{pthread_detach(pthread_self());uint64_t number = (uint64_t)args;std::cout << "pthread: " << number << " create success" << std::endl;while(true){pthread_mutex_lock(&mutex);// while(/*资源是否就绪*/) // ...// 不管临界资源的状态情况// 这里只是简单的进行打印信息。pthread_cond_wait(&cond, &mutex);std::cout << "pthread: " << number << " , cnt: " << cnt++ << std::endl;pthread_mutex_unlock(&mutex);}
}int main()
{for(uint64_t i = 0; i < 5; i++) //创建5个线程{pthread_t tid;pthread_create(&tid, nullptr, Count, (void*)i);usleep(1000);}sleep(3);std::cout << "main thread ctrl begin: " << std::endl;while(true) {sleep(1);pthread_cond_signal(&cond); //唤醒在cond的等待队列中等待的一个线程,默认都是第一个std::cout << "signal one thread..." << std::endl;}return 0;
}

2.2 条件变量小结

  • 条件变量的缺陷

    1. 复杂性:条件变量的使用比简单的互斥锁复杂,需要正确的同步和逻辑来避免竞争条件和死锁。

    2. 虚假唤醒(Spurious Wakeups):条件变量可能会在没有明确通知的情况下唤醒等待线程,即假唤醒。需要额外的逻辑来处理这种情况,通常是使用循环来反复检查条件

      为什么会出现伪唤醒呢?

      来看这样一个示例:现在每一个线程进入临界区都会判断一个队列(这个队列是一个临界资源)是否为空,如果队列为空那么就需要在条件变量下等待;否则就向下执行。一个线程A来到临界区的时候,发现队列为空,其到了条件变量下等待,此时另外一个线程B往队列中放入了数据,该线程释放锁资源并且唤醒了条件变量下等待的线程(原因所在)。此时如果有另外一个线程C进入了临界区,申请到了锁资源之后判断条件是满足的(因为另外一个线程B已经放了数据)所以线程C就继续向下执行,这个时候线程A申请锁资源是失败的(锁已经被线程C拿走了)所以线程A就阻塞等待。当线程C使用完了队列中的数据之后,此时队列又为空了。线程C释放锁资源,此时线程A获取到了锁资源!但是此时的队列已经为空了!!!所以线程A还需要再检查是否资源就绪。这就是我们需要用循环来判断条件是否就绪的原因

    3. 资源开销:条件变量的实现可能涉及系统调用,导致额外的资源开销和性能影响。

    4. 等待条件的复杂管理:条件变量要求等待条件在某些情况下能够正确地重新检查,这通常通过循环来实现,但增加了代码的复杂性。

http://www.dtcms.com/a/483417.html

相关文章:

  • 网站 301做电脑游戏破解的网站
  • 软件培训网站个人不良信息举报网站
  • 深圳品牌网站策划网站流量一直下降
  • Qiankun 主子应用通信方式对比及使用场景【前端微前端实战指南】
  • 二级域名网站优化肥城网站建设费用
  • 网站模板下载后怎么使用网络规划设计师 高级
  • python高效采集淘宝商品数据,详情页实时 API 接口接入
  • 个人房产信息查询网站企业查查官网登录入口
  • 沈阳制作网站的公司四平做网站佳业
  • Thinkphp8 Redis队列与消息队列topthink/think-queue 原创
  • LeetCode每日一题——螺旋矩阵
  • lamp网站开发实战工程机械网官网
  • .net AI MCP 入门 适用于模型上下文协议的 C# SDK 简介(MCP)
  • 做网站哪里需要用钱dedecms做电影网站
  • ZYNQ裸机开发指南笔记
  • Starlake:一款免费开源的ETL数据管道工具
  • 线性代数 | 要义 / 本质 (上篇)
  • 求网站建设和网页设计的电子书自己怎么给网站做优化
  • DM常用命令
  • 有趣的网站代码短视频运营公司网站建设
  • 网站模板二次开发网站怎么投放广告
  • Symmetric functions and hall polynomials 1.1 总结
  • 学好网页设计与网站建设的意义北京的软件公司
  • TCP三次握手与四次挥手详解
  • C++智能指针解析
  • Java 大视界 -- Java 大数据中的时间序列预测算法在金融市场波动预测中的应用与优化
  • 如何看网站关键词用discuz做的手机网站
  • 使用spring-ai时遇到的一些问题
  • 基于 recorder-core 的实时音频流与声纹识别技术实践
  • 成都没有做网站的公司详谈电商网站建设四大流程