【Linux线程(二)】线程互斥和同步

前言:

在上一篇博客中,我们讲解了什么是线程以及如何对线程进行控制,那么了解了这些,我们就可以在程序中创建多线程了,可是多线程往往会带有许多问题,比如竞态条件、死锁、数据竞争、内存泄漏等问题,解决这些问题的关键在于如何实现线程的互斥和同步

互斥:

  • 互斥是指一次只允许一个线程访问共享资源。这意味着当一个线程正在访问共享资源时,其他线程必须等待,直到该线程释放了资源。
  • 互斥通常通过互斥锁来实现。当一个线程获得了互斥锁时,其他线程就无法获得该锁,只能等待锁被释放。

同步:

  • 同步是指协调多个线程的执行顺序,以确保它们按照预期的顺序执行。
  • 同步机制可以确保在多个线程之间正确的共享信息和控制流。常见的同步机制包括信号量、条件变量和屏障等。
  • 同步通常用于控制线程之间的竞态条件和避免数据竞争的发生。

今天我们从多线程中的数据竞争问题入手,进一步了解多线程并且利用互斥机制来解决问题。

(一)多线程中的数据竞争

1.相关概念

在分析多线程中的数据竞争问题之前,需要先了解一些相关的概念:

并发访问:

并发指的是在一段时间内,多个任务交替地执行,这些任务可能在同一时间段内启动和执行,但并不一定同时执行。

临界资源&临界区:

  • 临界资源是指在多线程环境下需要互斥访问的共享资源,例如共享变量、共享内存区域、文件等。如果多个线程并发地访问和修改临界资源,可能会导致数据竞争和程序错误。
  • 临界区是指包含对临界资源访问的代码段或程序区域,这些代码段在任何给定时间点只能被一个线程执行,以确保对临界资源的安全访问。

原子性:

原子性是指在并发编程中操作的不可分割性,即一个操作要么完全执行,要么不执行,不存在中间状态。原子操作在执行过程中不会被中断,也不会被其他线程的操作干扰。

锁:

在并发编程中,(Lock)是一种同步机制,用于控制对临界区的访问,确保在任何给定时间点只有一个线程可以进入临界区执行代码。锁主要用于解决多线程环境下的竞态条件和数据竞争问题。

 2.多线程抢票场景

在日常生活中,高铁、火车抢票是很平常的一件事。假设票总量为1000,用户进入系统,如果剩余票的数量大于0,那么就代表还有票,用户抢到一张票,剩余票数量减一。这个场景其实就是多线程并发访问的场景,每个用户就代表一个线程,票代表共享资源,下面我们用代码来模拟一下

int ticket = 10000;
void *StartRoutine(void *args)
{const string name = static_cast<char*>(args);while(true){if(ticket > 0){cout<<name<<" get a ticket:"<<ticket<<endl;ticket--;}else{break;}usleep(1000);}return nullptr;
}
int main()
{pthread_t td1,td2,td3,td4;pthread_create(&td1,nullptr,StartRoutine,(void*)"thread-1");pthread_create(&td2,nullptr,StartRoutine,(void*)"thread-2");pthread_create(&td3,nullptr,StartRoutine,(void*)"thread-3");pthread_create(&td4,nullptr,StartRoutine,(void*)"thread-4");pthread_join(td1,nullptr);pthread_join(td2,nullptr);pthread_join(td3,nullptr);pthread_join(td4,nullptr);return 0;
}

可是当我们运行程序时,却发现运行的结果并不完全一致,有时候票的编号甚至会减少到0或-1、-2,这是为什么呢?

显然我们这个程序的多线程并发访问共享数据是有问题的。

3.并发访问问题分析

在这个程序中,对全局变量ticket进行访问的操作有: if(ticket > 0) 和 ticket--;

如果想要对共享数据进行操作,至少要分为三步:

  1. 将内存中数据的值拷贝到CPU中的寄存器中。
  2. 在CPU内部通过对寄存器的运算完成操作。
  3. 将寄存器中的结果拷贝回内存中。

大致图解如下: 

如果是在单线程中,上面这三步操作并不会被打断,可是在多线程中,由于上面的操作并不是原子的,而且线程会被调度,所以在中间可能会被打断。

比如线程A对ticket进行--操作,在执行完第二步后,寄存器中的内容已经由100减到99了,然后线程A将要执行第三步时,却发生了线程的调度,例如线程A的时间片到了,然后线程A会保存上下文数据并切走,保存上下文就是将数据单独给自己一份。

这时线程B会开始它对ticket的操作,并且线程B执行的很顺利,在线程A调度完成返回时,线程B已经完成了好几轮操作,内存中的数据被修改只剩1了,这时候线程A回来将继续执行第三步,它会将自己上下文中的数据拷贝回内存,这时候ticket又会编程99,所以多线程中并发访问是不安全的。

(二)互斥锁

通过上面的问题分析,我们要想安全的使用多线程,必须对多个线程都需要访问的共享资源进行保护,也就是将共享资源转变为临界资源。这样就可以实现线程的互斥,通常情况下我们可以使用信号量、条件变量、原子操作、互斥锁、读写锁等,今天我们利用互斥锁来实现线程互斥。

互斥锁(Mutex)是一种常见的同步机制,用于保护临界资源,确保在任何给定时间点只有一个线程能够访问临界资源。其基本原理是在进入临界区之前先锁定互斥锁,然后在退出临界区时释放锁。

1.互斥锁的初始化

我们既可以在程序中定义全局的锁,也可以定义局部的锁,如果使用全局锁,就需要

       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

进行初始化

       #include <pthread.h>int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);

 参数

  • pthread_mutex_t *restrict mutex:一个指向锁的指针。
  • const pthread_mutexattr_t *restrict attr):是一个互斥锁属性对象的类型,用于指定互斥锁的属性,不需要设置时可以传入nullptr。

返回值: 

  • 返回值为0:表示函数执行成功,互斥锁初始化成功。
  • 返回值为正数(通常是正整数):表示函数执行失败,但没有明确的错误码定义,具体含义可以根据系统的文档或头文件进行查找。
  • 返回值为负数:表示函数执行失败,并且会返回一个标准的错误码,可以通过 errno 全局变量获取,具体的错误码可以通过查看系统头文件 <errno.h> 来获得。

2.互斥锁的释放

释放一个锁可以通过pthread_mutex_destroy() 函数来实现

       #include <pthread.h>int pthread_mutex_destroy(pthread_mutex_t *mutex);

返回值:

  • 返回值为0:表示函数执行成功,互斥锁销毁成功。
  • 返回值为正数(通常是正整数):表示函数执行失败,但没有明确的错误码定义,具体含义可以根据系统的文档或头文件进行查找。
  • 返回值为负数:表示函数执行失败,并且会返回一个标准的错误码,可以通过 errno 全局变量获取,具体的错误码可以通过查看系统头文件 <errno.h> 来获得。

3.互斥锁加锁

对临界资源加锁,需要使用pthread_mutex_lock()函数,它用于获取(加锁)互斥锁的函数。它的作用是在进入临界区之前,尝试获取互斥锁,如果互斥锁已经被其他线程持有,则当前线程会被阻塞,直到获取到互斥锁为止。

       #include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);

返回值:

  • 返回值为0表示函数执行成功,当前线程成功获取了互斥锁。
  • 返回值为正数表示函数执行失败,但没有明确的错误码定义,具体含义可以根据系统的文档或头文件进行查找。
  • 返回值为负数表示函数执行失败,并且会返回一个标准的错误码,可以通过 errno 全局变量获取,具体的错误码可以通过查看系统头文件 <errno.h> 来获得。

4.互斥锁解锁

对临界资源解锁,需要使用pthread_mutex_unlock()函数,它是用于释放(解锁)互斥锁的函数。它的作用是在临界区代码执行完毕后,释放互斥锁,以便其他线程可以获取到互斥锁进入临界区执行代码。

       #include <pthread.h>int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:

  • 返回值为0表示函数执行成功,互斥锁成功释放。
  • 返回值为正数表示函数执行失败,但没有明确的错误码定义,具体含义可以根据系统的文档或头文件进行查找。
  • 返回值为负数表示函数执行失败,并且会返回一个标准的错误码,可以通过 errno 全局变量获取,具体的错误码可以通过查看系统头文件 <errno.h> 来获得。

5.代码示例

pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;int ticket = 1000;void *StartRoutine(void *args)
{const string name = static_cast<char *>(args);while (true){pthread_mutex_lock(&_mutex);if (ticket > 0){cout << name << " get a ticket:" << ticket << endl;ticket--;sum++;pthread_mutex_unlock(&_mutex);}else{pthread_mutex_unlock(&_mutex);break;}usleep(1000);}return nullptr;
}
int main()
{pthread_mutex_init(&_mutex, nullptr);pthread_t td1, td2, td3, td4;pthread_create(&td1, nullptr, StartRoutine, (void *)"thread-1");pthread_create(&td2, nullptr, StartRoutine, (void *)"thread-2");pthread_join(td1, nullptr);pthread_join(td2, nullptr);pthread_mutex_destroy(&_mutex);return 0;
}

上面的代码利用互斥锁实现了线程的互斥,使得在多个线程抢票的时候不会出现数据竞争的问题,也就不会让票的数量出现异常。

(三)锁的本质

大多数体系结构都提供了exchange或swap命令,该指令的作用是将寄存器和内存单元的数据进行交换,这个交换过程是原子的。

将pthread_mutex_lock()函数的汇编代码抽象出来

lock:movb $0, %alxchgb %al, mutexif(al寄存器里的内容 > 0){return 0;} else挂起等待;goto lock;

 

xchgb作用:将一个共享的mutex资源,交换到自己的上下文中,属于线程自己 。

这段伪代码的过程可以概括为: 

  • movb $0, %al:将0赋值给al寄存器中。
  • xchgb %al, mutex :将mutex的值赋值给al寄存器,我们默认mutex是1(大于0的值)。
  • 判断al寄存器中的数据是否大于0,如果大于0返回0,代表获取锁成功;如果小于0,就挂起等待,代表锁已经被别人获取了。

再看pthread_mutex_unlock()函数

unlcok:movb $l,mutex 唤醒等待Mutex的线程;return 0;
  • 将线程上下文中mutex资源跟内存中的mutex交换。

我对加锁解锁的理解就是:将锁看作一把钥匙。临界区看作一间房子,钥匙原本挂在房子里,第一个进入的线程会把钥匙放到自己口袋(上下文)里,如果线程在临界区被调度走,它会把钥匙也带走并关上房门,这样别的线程想要进来但是没有钥匙,而有钥匙的线程回来时还能够进入房子里,当线程解锁,就将钥匙放回到房子里,并打开房门。

(四)死锁

1.概念

死锁是在并发系统中的一种常见问题,它指的是两个或多个进程或线程因相互持有对方所需的资源而无法继续执行的状态。在死锁状态下,各进程或线程都在等待其他进程或线程释放资源,而导致它们都无法继续执行,从而形成了一种僵局。

2.必要条件

  • 互斥条件:一个资源只能每次只能被一个执行流使用。

  • 请求与保持条件:一个执行流因为请求资源而阻塞时,对已获得的资源保持不放。

  • 不剥夺条件:一个执行流已获得的资源,在未使用之前,不能强行剥夺。

  • 循环等待条件:若干执行流之间形成一种头尾相连的循环等待资源的关系。

3.解决方法

  • 资源分配策略:设计合理的资源分配策略,避免同时持有多个资源,从而减少死锁的发生可能性。
  • 加锁顺序:确保所有进程或线程都以相同的顺序请求资源,从而避免形成循环等待。
  • 超时机制:对于资源请求,设置超时机制,如果超过一定时间仍未能获取资源,则放弃当前请求,避免长时间等待而导致死锁。
  • 死锁检测和解除:定期检测系统中是否存在死锁,并采取相应的措施来解除死锁,例如终止部分进程或线程,释放资源等。

(五)线程同步

1.同步概念

在上面的互斥示例程序中,我们用抢票的例子来实现线程互斥,可是在打印时却有一个现象:总是有一个线程会抢占大多数的票,导致了其他线程一直在等待,抢不上票。这种现象叫做线程饥饿问题,指的是一个或多个线程无法获得所需的资源或者无法被调度执行而长时间等待

那么如何解决饥饿问题呢?这就需要同步了。接下来我用一个比喻来理解线程同步。

假设有一个VIP自习室,每次只能让一个同学进入学习,只要自习室内有人,别的同学就无法进入

 张三在一天的早上6点第一个到达自习室,并且一直在自习室里学习,这就表示张三一直在使用里面的资源。到了中午12点,张三想去吃饭,但是吃饭出去的话就必须将钥匙归还,然后吃完饭回来就得等待其他同学出来,可张三并不想等,所以张三又饿又不想出去,他将钥匙放回去又拿回来,在自习室里反复横跳。最后外面的同学一直没等到钥匙,没吃上饭,而张三一直持有钥匙,也没吃上饭,这就导致了其他同学的饥饿问题。

管理人员知道了这件事,定了下面两条规矩:

  • 刚把钥匙归还的同学不能再次立即申请钥匙。
  • 在外面等待钥匙的同学必须排队。

定了这两条规矩,张三再也不会反复横跳了,也就不会导致饥饿问题了。这就是利用线程同步。

线程同步是指在多线程环境中,对共享资源的访问进行协调和管理,以确保线程之间的正确交互和数据一致性。在并发编程中,线程同步是至关重要的,因为多个线程同时访问共享资源可能导致数据竞争和不确定性的结果。

2.条件变量

条件变量是在多线程编程环境中用来线程通信的机制,它通常和互斥锁使用来实现线程同步的效果。

条件变量实现了线程的等待和通知机制:

  • 等待(Wait):线程在等待条件变量时会释放它所持有的互斥锁,并进入阻塞状态,直到其他线程通知条件变量满足了某个条件。

  • 通知(Notify):线程在某个条件发生变化时可以通过条件变量通知等待条件变量的一个或多个线程,以唤醒它们继续执行。

2.1创建和销毁

       #include <pthread.h>int pthread_cond_destroy(pthread_cond_t *cond);int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

2.2等待

       #include <pthread.h>int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

 pthread_cond_wait 函数会使当前线程等待在指定的条件变量 cond 上,同时会释放传入的互斥锁 mutex,并将当前线程置于等待状态,直到有其他线程调用 pthread_cond_signalpthread_cond_broadcast 来唤醒它,或者出现了异常情况(如信号中断)。

2.3唤醒

       #include <pthread.h>int pthread_cond_signal(pthread_cond_t *cond);
       #include <pthread.h>int pthread_cond_broadcast(pthread_cond_t *cond);
  1. pthread_cond_signal

    • pthread_cond_signal函数用于唤醒等待在条件变量上的一个线程。
    • 如果有多个线程等待在条件变量上,调用pthread_cond_signal只会唤醒其中一个线程,具体唤醒哪个线程由系统决定(通常是按照先等待先唤醒的顺序)。
    • 如果没有线程等待在条件变量上,调用pthread_cond_signal也不会产生任何效果。
  2. pthread_cond_broadcast

    • pthread_cond_broadcast函数用于唤醒等待在条件变量上的所有线程。
    • 调用pthread_cond_broadcast会唤醒所有等待在条件变量上的线程,使它们都可以继续执行。
    • 如果没有线程等待在条件变量上,调用pthread_cond_broadcast也不会产生任何效果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/837413.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

高扩展性便携式1553B总线测试仪,支持麒麟操作系统

手提式便携1553B总线测试仪&#xff0c;利用订制平台的PXIe扩展槽嵌入石竹科技自主研发的高性能T系列专用1553B测试板卡和高级协议分析和测试软件FP-1553B Pro、FP-5186集成的一款模块化、功能可订制的测试仪器。 基本配置可对1553B信号进行波形采集&#xff08;提供软件示波器…

【批量处理文档】CleverPDF,WOED,EXCEL,PPT,PDF批量转换相关的工具网站NO.97

本文一共:429 个字,需要阅读:2 分钟,更新时间:2024年5 月14日,部分内容具有时效性,如有失效请留言,阅读量:1 CleverPDF&#xff0c;具有44个功能强大的免费在线PDF转换器和工具。 资源来源于网络&#xff0c;免费分享仅供学习和测试使用&#xff0c;请勿用于商业用途&#xff…

超级漂亮的 Ai 官网源码

超级漂亮的 AI 官网源码 效果图部分源码领取源码下期更新预报 效果图 部分源码 <script lang"js">function project3D(x, y, z, vars) {var p, dx - vars.camXy - vars.camY - 8z - vars.camZp Math.atan2(x, z)d Math.sqrt(x * x z * z)x Math.sin(p - v…

List/ConcurrentBag Contains的效率比Dictionary/ConcurrentDictionary ContainsKey 低几千倍

测试目标&#xff1a; 用两组 50000 个字符串做包含(Contains)判断&#xff0c;分析性能差异。 50000个字符串集合测试结果&#xff1a; Dictionary/ConcurrentDictionary 能在1-2毫秒完成50000个字符串集合的包含判断&#xff1b; List 需要3秒&#xff0c;ConcurrentBag需要…

白话机器学习4:小波分解的原理与Python代码实现

小波去噪可以想象成使用一把“筛子”来过滤信号。这个“筛子”能够根据信号的不同频率成分&#xff0c;将其分解成多个层次。在这个过程中&#xff0c;信号的重要信息通常包含在低频部分&#xff0c;而噪声则多分布在高频部分。 将信号通过这个“筛子”分解后&#xff0c;我们可…

leetcode-最长公共子序列(二)-103

题目要求 思路 step 1&#xff1a;优先检查特殊情况。 step 2&#xff1a;获取最长公共子序列的长度可以使用动态规划&#xff0c;我们以dp[i][j]dp[i][j]dp[i][j]表示在s1中以iii结尾&#xff0c;s2中以jjj结尾的字符串的最长公共子序列长度。 step 3&#xff1a;遍历两个字…

同为科技详解智能PDU所应用的通信协议与接口

现如今&#xff0c;信息服务、AI人工智能的飞速发展与增长&#xff0c;全球正经历信息数据的爆炸。不仅数据量以惊人的速度增长&#xff0c;而且全球社会各行业对数据的依赖的程度也在日益增加。这些趋势使数据中心在全球都享有关键基础架构的地位。假设某个数据中心发生严重的…

数据采集为什么会用到代理IP?

在数据采集中&#xff0c;代理IP是指通过使用代理服务器来隐藏或更改真实的IP地址&#xff0c;以访问目标网站或服务器。那么&#xff0c;数据采集为什么会用到代理IP呢&#xff1f;使用代理IP通常用于匿名地访问网站、绕过访问限制或提高数据采集的效率和安全性。 代理服务器作…

HCIA和HCIP区别大吗?小白请看这

华为认证以其专业性和实用性受到了业界的广泛认可。 HCIA、HCIP、HCIP这三个级别&#xff0c;你会选哪个&#xff1f;IE含金量不用多说&#xff0c;IA还是IP&#xff0c;你会纠结吗。 但面对这两个级别的认证&#xff0c;初学者或者“小白”们可能会感到困惑&#xff1a;两者…

以大开放促进大开发 | 陕西粮农集团携手开源网安引领新时代西部大开发

​5月13日&#xff0c;开源网安与陕西粮农集团成功签署战略合作协议。双方将在网络安全保障体系建设及人才培养领域展开深度合作&#xff0c;共同筑牢陕西省数字经济建设安全屏障。陕西省粮农信息技术有限公司总经理解玮峰、陕西省粮农信息技术有限公司安全事业部负责人马德君、…

易康001:易康多尺度分割结果异常

前言 易康是一种在遥感领域常用的数据处理软件&#xff0c;它主要是用于面向对象的分类&#xff0c;涵盖了分割、模糊分类、监督分类等流程。但是在进行多尺度分割时&#xff0c;往往会遇到一些问题&#xff0c;例如下面图片所示&#xff1a; 1 多尺度分割问题 这种问题一般是…

品鉴中的品鉴笔记:如何记录和分享自己的品鉴心得

品鉴云仓酒庄雷盛红酒的过程&#xff0c;不仅是品尝美酒&#xff0c;更是一次与葡萄酒深度对话的旅程。为了更好地记录和分享自己的品鉴心得&#xff0c;养成写品鉴笔记的习惯是十分必要的。 首先&#xff0c;选择一个适合的记录工具。可以是传统的笔记本&#xff0c;也可以是…

3DGS+3D Tiles融合已成 ,更大的场景,更细腻的效果~

最近国外同行Kieran Farr发布了一个他制作的3D GussianSplatting(高斯泼溅)Google Map 3D Tiles的融合叠加的demo案例&#xff08;如下所示&#xff09;。 准确来说这是一个数据融合的实景场景&#xff0c;该实景场景使用了倾斜三维和3D GussianSplatting两种实景表达技术&…

内存卡惊现0字节!数据丢失怎么办?

在日常使用电子设备的过程中&#xff0c;有时我们会遇到一个令人困惑的问题——内存卡突然变成了0字节。这意味着原本存储在内存卡中的数据似乎在一夜之间消失得无影无踪&#xff0c;给用户带来极大的困扰。本文将详细解析内存卡0字节现象&#xff0c;探究其原因&#xff0c;并…

【高阶数据结构】并查集 {并查集原理;并查集优化;并查集实现;并查集应用}

一、并查集原理 在一些应用问题中&#xff0c;需要将n个不同的元素划分成一些不相交的集合。开始时&#xff0c;每个元素自成一个单元素集合&#xff0c;然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一个元素归属于那个集合的运算。适合于描述这类…

构建NFS远程共享存储

目录 一. NFS介绍 二. 网络附加存储&#xff08;NAS&#xff09;设备 三. 远程过程调用 四. 实验测试 4.1 nfs-server操作 4.1.1 新建一个目录作为共享 4.1.2 新增一个磁盘作为共享 4.2 web1 客户端操作 一. NFS介绍 NFS&#xff08;Network File System&#xff09…

UTONMOS探索元宇宙:开启未来数字世界的无限可能

在科技的浪潮中&#xff0c;元宇宙如同一颗璀璨的星辰&#xff0c;冉冉升起&#xff0c;吸引着无数人的目光。 元宇宙&#xff0c;一个超越现实的数字世界&#xff0c;它融合了虚拟现实、增强现实和互联网等多种技术&#xff0c;为人们打造了一个全新的沉浸式体验空间。在这里…

数据结构--顺序表和链表的区别

顺序表和链表之间各有优劣&#xff0c;我们不能以偏概全&#xff0c;所以我们在使用时要关注任务的注重点&#xff0c;以此来确定我们要使用两者中的哪一个。 不同点&#xff1a; 存储空间上&#xff1a; 顺序表在物理结构上是一定连续的&#xff0c;而链表(这里以带头双向循环…

面 试 题

过滤器和拦截器的区别 都是 Aop 思想的一种体现&#xff0c;用来解决项目中 某一类 问题的两种接口(工具)&#xff0c;都可以对请求做一些增强 出身 过滤器来自 servlet 拦截器来自 spring 使用范围 过滤器 Filter 实现了 iavax.servlet.Filter 接口&#xff0c;也就是说…

CSS实现渐变色

渐变色分为线性渐变和径向渐变。 线性渐变linear-gradient(方向, 颜色1, 颜色2, … ,颜色n)径向渐变radial-gradient(颜色1 覆盖区域大小, 颜色2 覆盖区域大小, … ) 线性渐变的方向可以为&#xff1a; ​ 1、一个方向值时&#xff1a; to bottom 表示从上边到下边渐变 ​ 2、…