Linux之线程互斥

目录

一、问题引入

二、线程互斥

1、相关概念

2、加锁保护

1、静态分配

2、动态分配

3、锁的原理

4、死锁

三、可重入与线程安全

1、概念

2、常见的线程不安全的情况

3、常见的线程安全的情况

4、常见不可重入的情况

5、常见可重入的情况

6、可重入与线程安全联系

7、可重入与线程安全区别


一、问题引入

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。

我们来看看下面的多线程抢票系统的代码:

#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <pthread.h>using namespace std;int ticket = 100;void *getticket(void *arg)
{char *name = (char *)arg;while (true){if (ticket > 0){usleep(1000);cout << name << ":"<< " " << ticket << endl;ticket--;}elsebreak;}
}int main()
{pthread_t tid1, tid2, tid3, tid4;pthread_create(&tid1, nullptr, getticket, (void *)"thread 1");pthread_create(&tid2, nullptr, getticket, (void *)"thread 2");pthread_create(&tid3, nullptr, getticket, (void *)"thread 3");pthread_create(&tid4, nullptr, getticket, (void *)"thread 4");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);pthread_join(tid4, nullptr);return 0;
}

这里的ticket变量是一个全局变量,那么它就会被所有线程共享。创建线程后,所有线程访问getticket函数,对其进行了重入,访问ticket并对ticket--。但是,我们发现,票数出现了负数,这完全不符合我们的代码逻辑和想要的结果。这是为什么呢?

首先,程序在编译的时候会被编译成汇编代码, 而在汇编代码中,ticket--操作在我们看来只有一行代码,但是在汇编中它其实分为了三步:1、将ticket值拷入到CPU寄存器中;2、CPU对其进行--操作;3、将结果写回内存。

而我们知道进程是有时间片的,在执行完上面任意一步时,线程可能因为时间片到了而被切换。而这就会造成一些问题。如下图:

线程A先进入,在完成第二步 -- 操作后,因为时间片到了,要被切换出去,99作为上下文数据被保存起来随A一起被切换。线程B进入,因为B的时间片比较长,他把ticket值减到了50并写回了内存后,时间片到了,被切换。线程A再次进入CPU,把上下文恢复,然后接着第3步执行,直接把99写到了内存里面。

线程B明明已经让ticket的值减到了50,结果你个线程A又直接把结果改成了99。这样就出现了数据错乱的现象。

在我们对ticket进行并发访问的时候,由于ticket- - 操作并不是原子的,所以出现了数据不一致的情况。这种情况怎么解决呢?我们接着往下讲。

二、线程互斥

1、相关概念

1、临界资源:多线程执行流共享的资源就叫做临界资源。
2、临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
3、互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
4、原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

2、加锁保护

为了解决上面代码的数据不一致的问题,需要做到三点:

1、代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

2、如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。

3、如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

而其中最简单的一种方法就是对临界资源进行加锁保护。以达到下面的效果:

定义和初始化锁的函数: 

NAMEpthread_mutex_destroy, pthread_mutex_init - destroy and initialize a mutexSYNOPSIS#include <pthread.h>1、int pthread_mutex_destroy(pthread_mutex_t *mutex);2、int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);3、pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t 是由原生线程库给用户提供的一个数据类型,就是我们常说的锁。上图的 1和2 是对锁进行局部定义时的销毁和初始化操作,相当于析构函数和构造函数。

上图的 3 是对全局锁或者static静态锁进行初始化的方式。下面我们一一讲解。

加锁和解锁函数:

发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请锁,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁,再去申请锁。

NAMEpthread_mutex_lock,  pthread_mutex_trylock,  pthread_mutex_unlock  -  lock   andunlock a mutexSYNOPSIS#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_trylock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);

1、静态分配

静态分配就是我们 3 对应的对锁定义和初始化的方式。我们使用它对抢票代码进行保护。

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <time.h>
#include <pthread.h>using namespace std;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int ticket = 100;void *getticket(void *arg)
{char *name = (char *)arg;while (true){pthread_mutex_lock(&mutex); // 加锁保护,其他线程只能在这阻塞等待,直到拿到锁if (ticket > 0)             // 这部分代码只能串行执行{usleep(rand() % 10000);cout << name << ":"<< " " << ticket << endl;ticket--;pthread_mutex_unlock(&mutex); // 访问完临界资源,解锁,// 让其他线程能够拿锁访问}else{pthread_mutex_unlock(&mutex); // 访问完临界资源,解锁// 让其他线程能够拿锁访问break;}usleep(rand() % 2000000);}return nullptr;
}int main()
{srand((unsigned long)time(nullptr) ^ getpid() ^ 433);pthread_t tid1, tid2, tid3, tid4;pthread_create(&tid1, nullptr, getticket, (void *)"thread 1");pthread_create(&tid2, nullptr, getticket, (void *)"thread 2");pthread_create(&tid3, nullptr, getticket, (void *)"thread 3");pthread_create(&tid4, nullptr, getticket, (void *)"thread 4");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);pthread_join(tid4, nullptr);return 0;
}

注:加锁的时候,一定要保证加锁粒度越小越好。最好不要让一些非临界区也被加锁保护。

2、动态分配

如果我们定义的锁是一个局部变量,那么我们就要像下面的代码这样使用锁:

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <time.h>
#include <pthread.h>using namespace std;
#define THREAD_NUM 5class threaddata
{
public:threaddata(const string &s, pthread_mutex_t *m): name(s), mtx(m){}public:string name;pthread_mutex_t *mtx;
};int ticket = 100;void *getticket(void *arg)
{threaddata *td = (threaddata *)arg;while (true){pthread_mutex_lock(td->mtx);if (ticket > 0)              {usleep(rand() % 10000);cout << td->name << ":"<< " " << ticket << endl;ticket--;pthread_mutex_unlock(td->mtx);}else{pthread_mutex_unlock(td->mtx);break;}usleep(rand() % 2000000);}delete td;return nullptr;
}int main()
{pthread_mutex_t mtx;pthread_mutex_init(&mtx, nullptr);srand((unsigned long)time(nullptr) ^ getpid() ^ 433);pthread_t t[THREAD_NUM];for (int i = 0; i < THREAD_NUM; i++){string name = "thread ";name += to_string(i + 1);threaddata *td = new threaddata(name, &mtx);pthread_create(t + i, nullptr, getticket, (void *)td);}for (int i = 0; i < THREAD_NUM; i++)pthread_join(t[i], nullptr);pthread_mutex_destroy(&mtx);return 0;
}

3、锁的原理

通过加锁,我们能够保证执行临界资源的操作是原子的。可是,访问临界资源时,多个线程要申请同一把锁,那么就必须要能够看到同一把锁,那么这个锁不就成了一个临界资源了吗,那锁是怎么保证自己的安全的呢?

为了保证锁的安全,申请和释放锁的操作也必须是原子的。如何保证呢?

在汇编的角度,如果只有一行汇编语句,我们就认为该汇编语句的执行是原子的。一般来说,是使用swap或exchange指令,以一条汇编语句,将内存和CPU寄存器的数据进行交换。如下图:

线程a是第一个申请锁的。它先将 %al 的内容写成 0,然后交换 %al 和 mutex 的内容,%al 为 1,mutex为0。接着,判断%al的内容 >0,返回,成功拿到锁。线程a切出,寄存器%al的数据作为上下文随线程a一起切出。(当然,线程a可能在任何时候被切出,这是线程a时间片比较长的情况)。

线程b,接着申请锁。 它也先将 %al 的内容写成 0,然后交换 %al 和 mutex 的内容,%al 为 0,mutex为0。接着,判断%al的内容不大于0,于是线程b挂起等待。只有线程a将锁释放后,才能重新申请锁。

4、死锁

死锁:多线程场景中, 多个执行流彼此申请对方的锁资源,并且还不释放自己已申请的锁资源,进而导致执行流无法继续向下执行代码的现象。

产生死锁四个必要条件:
1、互斥条件:一个资源每次只能被一个执行流使用。
2、请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
4、循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

避免产生死锁:
1、破坏死锁的四个必要条件
2、加锁顺序一致
3、避免锁未释放的场景
4、资源一次性分配

三、可重入与线程安全

1、概念

~ 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

~ 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

2、常见的线程不安全的情况

1、不保护共享变量的函数。
2、函数状态随着被调用,状态发生变化的函数。
3、返回指向静态变量指针的函数。
4、调用线程不安全函数的函数。

3、常见不可重入的情况

1、调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。
2、调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
3、可重入函数体内使用了静态的数据结构。

4、可重入与线程安全联系

1、函数是可重入的,那就是线程安全的
2、函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
3、如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

5、可重入与线程安全区别

1、可重入函数是线程安全函数的一种
2、线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
3、如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

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

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

相关文章

L1-7 分寝室 [java]

分数 20 学校新建了宿舍楼&#xff0c;共有 n 间寝室。等待分配的学生中&#xff0c;有女生 n0​ 位、男生 n1​ 位。所有待分配的学生都必须分到一间寝室。所有的寝室都要分出去&#xff0c;最后不能有寝室留空。 现请你写程序完成寝室的自动分配。分配规则如下&#xff1a; …

测算土地面积并导出的新方法

让每一个人&#xff0c;都有自己的地图&#xff01; 我们在《如何测量显示多个距离和面积》一文中分享过测量距离和面积的方法&#xff0c;随着最近新版本的上线&#xff0c;该功能又有了新方法。 这里以测算土地面积并导出为例&#xff0c;分享这个新版本中的面积测量与导出…

什么是URL转码留痕功能(川圣SEO)#蜘蛛池

baidu搜索&#xff1a;如何联系八爪鱼SEO? baidu搜索&#xff1a;如何联系八爪鱼SEO? baidu搜索&#xff1a;如何联系八爪鱼SEO? URL转码留痕功能是一种用于关键词网址的编码转换技术&#xff0c;可以将包含特殊字符的URL转换为可读的格式&#xff0c;以便于搜索引擎收录和…

搭建Hadoop集群

一、前言 虚拟机&#xff08;Virtual Machine&#xff09;指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。在实体计算机中能够完成的工作在虚拟机中都能够实现。 虚拟机是在一些开发测试工作中常常需要用到的功能&#xff0c;常见的虚拟机…

通讯芯片D3232简介——主要用于工控主板、工业控制器、新能源充电桩等众多涉及RS232通讯的产品。

一、应用领域 D3232芯片主要用于工控主板、工业控制器、程序烧录下载器、仿真器、新能源充电桩等众多涉及RS232通讯的产品。 二、基本特性 D3232芯片由两个线路驱动器、两个线路接收器和双电荷泵电路组成&#xff0c;具有HBM>15kV、CDM>2kV的ESD保护能力&#xff0c;并且…

加速渲染:Blender与在线渲染农场的结合

​在数字艺术和三维设计的世界里&#xff0c;Blender软件因其强大的功能和灵活性而广受欢迎。然而&#xff0c;随着项目复杂性的增加&#xff0c;渲染时间也随之增长&#xff0c;成为艺术家和设计师面临的一大挑战。在线渲染农场的出现&#xff0c;为这一问题提供了革命性的解决…

使用点链云管家创建瑜伽约课小程序

点链云管家 点链云管家是由上海点链科技开发的门店管理系统&#xff0c;为线下门店商家提供一站式门店运营服务平台解决方案&#xff0c;适用于瑜伽健身、美业、新零售会员制电商、母婴店、宠物店、按摩养生、服装、美容、美甲、汽车服务、商超零售、餐饮、KTV娱乐、干洗等18个…

​​控制学习_有刷直流力矩电机的建模、仿真、控制带宽的讨论、驱动方式与选择

1 有刷直流力矩电机的建模 图1为有刷直流力矩电机的等效控制框图&#xff0c;其中 e i ( t ) {e_i}\left( t \right) ei​(t)为电机输入电压&#xff0c; L a {L_a} La​为电枢电感&#xff0c; R a {R_a} Ra​为电枢电阻&#xff0c; e m ( t ) {e_m}\left( t \right) em​(t)…

学了 Python 但又感觉没学 Python 不如重学 Python - day2(基础内置函数与变量引用的详细理解)

目录 1、int 函数 2、 bin、oct、hex 函数 3、 type 函数 4、complex 函数 5、布尔运算 6、chr 与 ord 函数 7、max 与 min 函数 8、eval 函数 9、变量对象引用 10、对象的垃圾回收 11、变量命名规则 12、 序列赋值 13、增强赋值 1、int 函数 按 n 进制将整数字符…

mac删除带锁标识的app

一 、我们这里要删除FortiClient.app 带锁 常规方式删除不掉带锁的 app【如下图】 二、删除命令&#xff0c;依次执行即可。 /bin/ls -dleO /Applications/FortiClient.app sudo /usr/bin/chflags -R noschg /Applications/FortiClient.app /bin/ls -dleO /Applications/Forti…

【漏洞复现】畅捷通T+ GetStoreWarehouseByStore接口处存在反序列化RCE漏洞

免责声明&#xff1a;文章来源互联网收集整理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该…

Python电梯楼层数字识别

程序示例精选 Python电梯楼层数字识别 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对《Python电梯楼层数字识别》编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0c;易读。 学习与应…