【Linux】线程互斥-创新互联
目录
🌸1、Linux线程互斥 🍧1.1、线程间互斥相关背景概念
🍨1.2、互斥量(锁)相关背景
🍯1.3、互斥量(锁)相关API 🍯1.3.1、初始化和销毁互斥锁
🍰1.3.2、互斥量加锁和解锁
🍲1.3.3、互斥锁的实现原理
🍁2、线程安全与重入函数 🍧2.1、概念
🍨2.2、常见线程安全情况
🍨2.3、常见重入情况
🍨2.3、可重入与线程安全
🍂3、常见锁概念
分享文章:【Linux】线程互斥-创新互联
转载注明:http://ybzwz.com/article/ccopgs.html
- 🌈前言
- 🌸1、Linux线程互斥
- 🍧1.1、线程间互斥相关背景概念
- 🍨1.2、互斥量(锁)相关背景
- 🍯1.3、互斥量(锁)相关API
- 🍯1.3.1、初始化和销毁互斥锁
- 🍰1.3.2、互斥量加锁和解锁
- 🍲1.3.3、互斥锁的实现原理
- 🍁2、线程安全与重入函数
- 🍧2.1、概念
- 🍨2.2、常见线程安全情况
- 🍨2.3、常见重入情况
- 🍨2.3、可重入与线程安全
- 🍂3、常见锁概念
这篇文章给大家带来线程同步与互斥的学习!!!
🌸1、Linux线程互斥 🍧1.1、线程间互斥相关背景概念
这些名词,我们在共享内存中已经了解过⭐⭐
-
概念
- 临界资源:多线程执行流共享(都能看到,并且能访问)的资源就叫做临界资源
- 临界区:每个线程执行流内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源(全局、静态变量、共享内存等),通常对临界资源起保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两种状态,要么完成,要么未完成,没有中间状态
🍨1.2、互斥量(锁)相关背景
-
互斥量mutex
- 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互
- 多个线程并发的操作共享变量,会带来一些问题(原子性问题)
验证:设置一个多线程来进行抢票,票数为共享资源 – 售票系统代码
#include#include#include#includeusing namespace std;
int ticket = 10000;
void *GrabTickets(void *args)
{// 多线程一直抢票,直到票数<=0为止
const char *name = static_cast(args);
while (true)
{if (ticket >0)
{usleep(1000);
printf("%s: 抢到了票, 票的编号为: %d\n", name, ticket);
ticket--;
}
else
{printf("%s: 已经放弃抢票了,因为没有了...\n", name);
break;
}
}
return nullptr;
}
int main()
{// 定义线程id
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
pthread_t tid4;
// 创建线程
if (pthread_create(&tid1, nullptr, GrabTickets, (void *)"Thread1") != 0)
{exit(EXIT_FAILURE);
}
if (pthread_create(&tid2, nullptr, GrabTickets, (void *)"Thread2") != 0)
{exit(EXIT_FAILURE);
}
if (pthread_create(&tid3, nullptr, GrabTickets, (void *)"Thread3") != 0)
{exit(EXIT_FAILURE);
}
if (pthread_create(&tid4, nullptr, GrabTickets, (void *)"Thread4") != 0)
{exit(EXIT_FAILURE);
}
// 线程等待 -- 不获取线程退出码
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);
pthread_join(tid4, nullptr);
return 0;
}
一次运行结果:出现溢出抢票的情况!!!
Thread3: 已经放弃抢票了,因为没有了...
Thread2: 抢到了票, 票的编号为: 3
Thread2: 已经放弃抢票了,因为没有了...
Thread4: 抢到了票, 票的编号为: -1
Thread4: 已经放弃抢票了,因为没有了...
Thread1: 抢到了票, 票的编号为: -2
Thread1: 已经放弃抢票了,因为没有了...
-
为什么可能无法获得正确的结果呢?
- if 语句判断条件为真以后,代码可以并发的切换到其他线程
- usleep函数用于模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段(代码区)
- ticke自减操作本身就不是一个原子操作(有中间动作,线程切换时会被挂起)
- CPU内的寄存器是被所有执行流(线程)共享的,但是寄存器里面的数据是属于当前执行流的上下文数据
- 线程被切换时,需要保存上下文数据。线程被换回时,要恢复上下文数据
// 取出ticket--部分的汇编代码
// 指令:objdump -d a.o >test.objdump
//-------------------------------------------------------------------------------------
44: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 4a<_Z11GrabTicketsPv+0x4a>4a: 83 e8 01 sub $0x1,%eax
4d: 89 05 00 00 00 00 mov %eax,0x0(%rip) # 53<_Z11GrabTicketsPv+0x53>
-
ticket自减操作对应三条汇编指令
- load :将共享变量ticket从内存加载到寄存器中
- update : 更新寄存器里面的值,执行-1操作
- store :将新值,从寄存器写回共享变量ticket的内存地址
为什么说ticket不是原子操作呢?
-
多线程访问共享资源问题
- 因为CPU在运算ticket自减操作时(比如计算完后),线程的时间片到了,需要进行线程切换,但是ticket计算完后的数据没有拷贝回内存,就被切换了
- 线程切换时,将保存线程的上下文,下一个线程运算完后,ticket的值变成9999
- 随后切换回原来的线程,恢复线程的上下文,将运算好的9999拷回内存的ticket中,导致数值不一样问题(应该变成9998)!
-
要解决以上问题,需要做到三点
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区
- 要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量
🍯1.3、互斥量(锁)相关API 🍯1.3.1、初始化和销毁互斥锁
-
互斥锁概念
- 互斥锁只能对临界区进行加锁,加锁的本质是让线程执行临界区代码串行化
- 加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加。如果一部分代码加,一部分不加,会出现bug
- 对临界区加锁时,加锁的粒度约细越好,否则可能出现死锁的情况(没有解锁)
- 加锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁
- 多线程竞争和申请锁的过程,就是原子的
初始化互斥量有二种方法
第一种方法:静态分配
#includepthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
-
代码解析
- pthread_mutex_t是互斥锁,它是一个联合体,里面有一个结构体描述锁的属性
- PTHREAD_MUTEX_INITIALIZER:它是一个宏,用于初始化互斥锁
第二个方法:动态分配
#includeint pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrictattr);
-
函数解析
- mutex:要初始化的互斥锁(pthread_mutex_t变量的地址)
- restrictattr:设置互斥锁的属性,一般为NULL/nullptr
- 返回值:初始化成功返回0,失败返回一个错误码errno
销毁互斥锁:
#include
-
函数解析
- mutex:要销毁的互斥锁(pthread_mutex_t变量的地址)
- 返回值:初始化成功返回0,失败返回一个错误码errno
销毁互斥锁需要注意
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥锁不需要销毁
- 不要销毁一个已经加锁的互斥锁
- 已经销毁的互斥锁 ,要确保后面不会有线程再尝试加锁
🍰1.3.2、互斥量加锁和解锁
加锁和解锁:
#include
-
函数解析
- mutex:要加锁或解锁的互斥量(pthread_mutex_t变量的地址)
- 返回值:初始化成功返回0,失败返回一个错误码errno
调用 pthread_ lock 时,可能会遇到以下情况:⭐⭐
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁
修改前面的售票系统代码:使用动态分配互斥锁,需要释放互斥锁(pthread_mutex_destroy)
#include#include#include#includeusing namespace std;
// 1、定义互斥锁,主线程初始化
pthread_mutex_t Mutex;
int ticket = 10000;
void *GrabTickets(void *args)
{const char *name = static_cast(args);
while (true)
{// 3、加锁
pthread_mutex_lock(&Mutex);
if (ticket >0)
{usleep(1000);
printf("%s: 抢到了票, 票的编号为: %d\n", name, ticket);
ticket--;
// 解锁 -- 互斥量的粒度越细越好
pthread_mutex_unlock(&Mutex);
}
else
{// 解锁 -- 如果没有解锁,线程再次加锁时,会一直阻塞
pthread_mutex_unlock(&Mutex);
printf("%s: 已经放弃抢票了,因为没有了...\n", name);
break;
}
}
return nullptr;
}
int main()
{// 2、初始化互斥锁 -- 动态分配互斥锁
pthread_mutex_init(&Mutex, nullptr);
// 定义线程id
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
pthread_t tid4;
// 创建线程
if (pthread_create(&tid1, nullptr, GrabTickets, (void *)"Thread1") != 0)
{exit(EXIT_FAILURE);
}
if (pthread_create(&tid2, nullptr, GrabTickets, (void *)"Thread2") != 0)
{exit(EXIT_FAILURE);
}
if (pthread_create(&tid3, nullptr, GrabTickets, (void *)"Thread3") != 0)
{exit(EXIT_FAILURE);
}
if (pthread_create(&tid4, nullptr, GrabTickets, (void *)"Thread4") != 0)
{exit(EXIT_FAILURE);
}
// 线程等待 -- 不获取线程退出码
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);
pthread_join(tid4, nullptr);
// 释放互斥锁 -- 动态申请的互斥锁
pthread_mutex_destroy(&Mutex);
return 0;
}
修改前面的售票系统代码:使用静态分配互斥锁,不需要释放互斥锁
#include#include#include#includeusing namespace std;
// 1、定义互斥锁,主线程初始化 -- 静态分配互斥锁
pthread_mutex_t Mutex = PTHREAD_MUTEX_INITIALIZER;
int ticket = 10000;
void *GrabTickets(void *args)
{const char *name = static_cast(args);
while (true)
{// 3、加锁
pthread_mutex_lock(&Mutex);
if (ticket >0)
{usleep(1000);
printf("%s: 抢到了票, 票的编号为: %d\n", name, ticket);
ticket--;
// 解锁 -- 互斥量的粒度越细越好
pthread_mutex_unlock(&Mutex);
}
else
{// 解锁 -- 如果没有解锁,线程再次加锁时,会一直阻塞
pthread_mutex_unlock(&Mutex);
printf("%s: 已经放弃抢票了,因为没有了...\n", name);
break;
}
}
return nullptr;
}
int main()
{// 2、初始化互斥锁
pthread_mutex_init(&Mutex, nullptr);
// 定义线程id
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
pthread_t tid4;
// 创建线程
if (pthread_create(&tid1, nullptr, GrabTickets, (void *)"Thread1") != 0)
{exit(EXIT_FAILURE);
}
if (pthread_create(&tid2, nullptr, GrabTickets, (void *)"Thread2") != 0)
{exit(EXIT_FAILURE);
}
if (pthread_create(&tid3, nullptr, GrabTickets, (void *)"Thread3") != 0)
{exit(EXIT_FAILURE);
}
if (pthread_create(&tid4, nullptr, GrabTickets, (void *)"Thread4") != 0)
{exit(EXIT_FAILURE);
}
// 线程等待 -- 不获取线程退出码
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);
pthread_join(tid4, nullptr);
return 0;
}
临界区的临界资源被锁后,当前线程时间片到了,还能进行线程间切换吗?加锁 == 不会切换?⭐⭐⭐
-
结论
- 完全可以切换,因为线程执行加锁解锁对应的也是代码
- 但是线程加锁是原子的,要么拿到锁,要么没拿到(多线程竞争申请互斥锁资源)
比如:我们有线程A和其他线程
- 线程A申请到了锁,执行临界区代码中途被切走了,切走时也是把锁抱走的
- 在线程A被切走的时候,绝对不会有线程进入临界区
- 因为进入临界区要申请互斥锁的资源,但是线程A已经申请了,其他线程只能一直阻塞等待资源就绪,然后竞争资源
- 线程A访问临界区只有进入和使用完毕二种状态(原子性),这样才对其他线程有意义
总结
- 不要再临界区做过多的事情,临界区代码最好越短越好
- 因为可能执行到一部分时,时间片就到了,然后其他线程一直阻塞等待,耗时长
🍲1.3.3、互斥锁的实现原理
-
前言
- 经过上面的例子,我们都已经意识到单纯的ticket++ 或者 ++ticket都不是原子的,有可能会导致数据一致性问题
- 为了实现互斥锁操作,大多数计算机体系结构都提供了swap或exchange汇编指令
- 该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性
- 即使是多处理器平台(多核CPU),访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期
- swap或exchange交换指令只有一句,意味着只有没做和做完二种状态,它是原子操作
当执行完二条汇编语句时,时间片到了,线程切换后,会出问题吗?
-
没有问题
当一个线程在执行时,CPU中一组寄存器中保存的值被称为该线程的上下文
- 因为线程切换时,会将寄存器中的数据全部带走!!!
- 凡是在寄存器中的数据,全部都是线程内部的上下文!!!
- 寄存器是在多线程看来,是共享的资源(CPU只有一套寄存器),但是在线程看来是自己的私有资源(因为线程会拿着寄存器的数据切换走)
- 多线程看起来同时在访问寄存器,但是它们互不影响
如果多线程同时竞争锁时,同时将0数据传输到al寄存器中,会出现问题吗?
-
比如:mutex = 1
- 不会出现问题,因为多线程中竞争资源时,至少有一个线程执行第二条交换指令
- 当第一个线程执行完这个指令后,寄存器的数据就会变成1,内存的数据变成0,而其他线程执行第二条指令,0跟0交换,没有发生变化
- 第一次执行交换指令的线程,会进入if语句,并且返回0表示申请锁成功,而其他线程会一直挂起/阻塞等待
🍁2、线程安全与重入函数 🍧2.1、概念
-
概念
- 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题
- 重入函数:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入
- 一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数
🍨2.2、常见线程安全情况
-
线程不安全情况
- 不保护共享变量(可以被全部线程访问的临界资源)的函数
- 函数状态随着被调用,状态发生变化的函数(计数器,记录函数被调用次数)
- 调用线程不安全函数的函数(一般不带_r的函数为不安全的)
- 返回指向静态变量指针的函数(多线程返回静态指针,导致数据不稳定)
-
线程安全情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
🍨2.3、常见重入情况
-
不可重入函数情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O库函数,标准I/O库的实现都以不可重入方式来管理全局数据结构的
- 可重入函数体内使用了静态的数据结构
-
可重入函数情况
- 不使用全局变量或静态变量,不使用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据(线程独立栈变量),或者通过制作全局数据的本地拷贝来保护全局数据
🍨2.3、可重入与线程安全
-
它们之间联系
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
-
它们之间的区别
- 线程安全是:描述多线程并发所带来的问题
- 可重入与不可重入是:描述函数是否可以重复进入,它们是不同的概念
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的
🍂3、常见锁概念
-
死锁
- 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态
死锁四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用\
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
-
避免死锁
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
分享文章:【Linux】线程互斥-创新互联
转载注明:http://ybzwz.com/article/ccopgs.html