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

linux线程同步与互斥

1. 线程互斥相关概念

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

2. 线程互斥

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量
归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完
成线程之间的交互。
但是多个线程并发的操作共享变量,会带来⼀些问题。
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;void *route(void *arg)
{char *id = (char *)arg;while (1){if (ticket > 0) // 1. 判断{usleep(1000);                               // 1. 模拟抢票花的时间printf("%s sells ticket:%d\n", id, ticket); // 2. 抢到了票ticket--;                                   // 3. 票数--}else{break;}}return nullptr;
}int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

为什么会减到负数?

首先,ticket--操作不是原子的

对于ticket--的原子性,要么--,要么不做--,不会存在中间状态,而CPU执行ticket--的汇编语言有三条语句,这三条汇编之间,任意一个位置线程都可能被中断切换,因此ticket--不是原子的。

0xFF00 mov ebx ticket  //将ticket的值传给寄存器
0xFF02 减少 ebx 1      //执行减一操作
0xFF04 写回 0x1111 ebx //将ebx计算后的值写回内存

 当一个线程执行ticket--时,需要执行上面三步,可能当该线程执行完第二步,在执行第三步之前,发生了线程切换,这个线程就会保存自己的上下文然后被放到系统的等待队列中了,如果没被其他线程打扰则正常完成ticket--操作。

假如一个线程A

//执行完第二步
ebx:99
PC指针:oxff04 

还有一个线程B运气比较好一直执行而没有被打断,直到执行到ticket = 1;

ebx:1
pc指针: 0xff02

然后发生线程切换,CPU开始执行线程A的内容,而线程A中保存的ticket=99,执行完第三步汇编后,将此时’99‘保存到内存中ticket对应的位置,这就导致线程B的努力全都白费了!!这就导致了数据不一致问题。

票数减到负数虽然与此有关,但主要矛盾其实是判断语句ticket>0

假如现在ticket=1,此时 线程1进入判断语句并通过,在usleep的时间里,有其他线程经过判断进入,然后被切走,之后线程2、线程3、线程4也是如此,然后执行线程1,ticket=0,线程2、3、4依次使ticket--,因此票数减到了负数!

从上面的例子中可以看出,全局资源没有加保护时,可能会有并发问题。

route函数因为有临界资源,所以是不可重入函数

线程切换与切回

什么时候会发生线程切换呢?1. 时间片到了 2.阻塞式IO 3.挂起或休眠的函数什么时候选择新的线程?从内核态返回用户态时,进行检查,条件不满足则选择新的线程。

简单的说,只有一条汇编语句是原子性的。 

那么怎么解决票数减为负数的问题呢?这就是线程锁的用处

// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;void *route(void *arg)
{char *id = (char *)arg;while (1){pthread_mutex_lock(&lock);if (ticket > 0) // 1. 判断{usleep(1000);                               // 1. 模拟抢票花的时间printf("%s sells ticket:%d\n", id, ticket); // 2. 抢到了票ticket--;                                   // 3. 票数--pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}}return nullptr;
}int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

3. 互斥锁/互斥量mutex

3.1 pthread_mutex_init/destroy

两种申请锁的方式,局部锁需要初始化和手动销毁,而全局锁不需要被手动释放,程序运行结束会自动释放。

#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *restrict mutex,          // 指向要初始化的互斥锁const pthread_mutexattr_t *restrict attr  // 属性参数(通常为NULL)
);// 静态初始化方式,全局
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
成功:返回 0。失败:返回错误码(非零值),例如:EAGAIN:系统资源不足。ENOMEM:内存不足。EPERM:权限不足(如进程无法设置优先级继承属性)
int pthread_mutex_destroy(pthread_mutex_t *mutex);返回值:
成功:返回 0。
失败:返回错误码(如 EBUSY 表示锁正在被占用)。

3.2 pthread_mutex_lock

函数	行为
pthread_mutex_lock	阻塞直到获得锁。
pthread_mutex_trylock	非阻塞,立即返回:成功获得锁返回 0,锁被占用返回 EBUSY。
pthread_mutex_unlock	释放锁,允许其他线程竞争。返回值:成功:返回 0。失败:返回错误码(如 EINVAL 表示锁未初始化,EDEADLK 表示死锁)。

无论是局部锁还是全局锁,申请锁前都需要先加锁,就是将申请的锁传递给pthread_mutex_lock加锁。

(1)关于加锁:

1.所有线程都需要竞争申请锁,多线程都得先看到锁,锁本身就是临界资源,因此申请锁的过程必须是原子的

2.申请锁成功则继续向后运行、访问临界区代码、访问临界资源,失败则阻塞挂起申请执行流。

(2)两个本质: 

锁提供的功能的本质:将执行临界区代码由并行转为串行。(这种在执行期间不会被打扰,也是原子性的一种表现)

对临界区资源的保护本质上就是用锁对临界区的代码进行保护。

(3)加锁之后的线程切换

加锁之后,在临界区内部也是允许线程切换的。但是当前线程是持有锁被动切换的,即使该线程不在,其他线程也得等到该线程回来执行完代码,释放锁后,才能展开所得竞争,进入临界区。

4. 锁的原理

硬件级实现:时钟中断

软件级实现:

为了实现互斥锁的操作,大多数体系结构都提供了swap和exchange指令,该指令的作用是把寄存器和内存单元的数据交换。(这两个指令只有一条汇编指令,保证了原子性)

锁其实就是一种标记位,可以把锁mutex当成一个整数,假如是1,表示该锁没有被线程申请。

上图中的%al是一个寄存器,在此代码中作为 临时变量,用于存储 mutex 的当前值,并判断锁的状态:0:锁被占用(其他线程持有)。1:锁空闲(可获取)。

lock与unlock

(1)lock:将0存入%al中,然后和锁的值进行交换,加入交换前锁空闲,则交换后%al的值为1,执行return 0, 否则挂起等待。

(2)将锁的值置为1,唤醒正在挂起等待的线程。

我们用swap、exchange将内存中的变量交换到CPU的寄存器中,本质上是当前进程/线程在获取锁,注意,是交换而不是拷贝!!!所以1只有1份,谁申请到,谁就持有锁。

包含线程切换的lock流程

线程切换时线程会将上下文内容保存起来,包括执行到哪段代码、该线程的%al里的内容等。

假如有线程A、B、C

线程A执行lock,执行完第一条语句将%al的值置为0(这是线程A的私有上下文内容),然后切换。此时线程A的%al为0, 内存中的mutex为1。 

然后线程B执行lock,执行第一句将%al的值清0(清理的都是自己的上下文数据),执行完第二条语句后切换。此时线程B的%al为1, 内存中的mutex为0。 

线程C执行lock,执行第一句将%al置为0,由于mutex为0,所以执行完第二条语句后%al仍然为0,通过条件判断进程C被阻塞挂起。此时线程C的%al为0, 内存中的mutex为0。 

然后切换到线程A,线程A阻塞挂起。切换到线程B,线程Breturn,线程B申请锁成功。

从上面的例子中可以看出,锁是原子性的,无论有没有线程切换、什么时候切换,都不影响锁的申请。

5.线程安与重入问题

(1)定义

线程安全 (Thread Safety)
定义:多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。一般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。

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

重入可以分为两种情况:1. 多线程重入函数 2.信号导致一个执行流重复进入函数

(2)线程安全与可重入性常见情况

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

不可重入的情况
1. 调用了malloc/free函数(使用全局链表管理堆)
2. 调用了标准I/O库函数(使用全局数据结构)
3. 函数体内使用了静态的数据结构

线程安全的情况
1. 线程对全局变量/静态变量只有读取权限
2. 类或接口对线程是原子操作
3. 线程切换不会导致接口执行结果出现二义性

可重入的情况
1. 不使用全局变量或静态变量
2. 不使用malloc/new开辟空间
3. 不调用不可重入函数
4. 不返回静态/全局数据(所有数据由调用者提供)
5. 使用本地数据或全局数据的本地副本

(3)可重入与线程安全联系
函数是可重入的,那就是线程安全的!!!
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果⼀个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

(4)可重入与线程安全区别

可重入函数是线程安全函数的⼀种

线程安全不⼀定是可重入的,而可重入函数则⼀定是线程安全的。 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果重入这个函数时若锁还未释放则会产生死锁,因此是不可重入的。
(5)注意
如果不考虑信号导致⼀个执行流重复进入函数这种重入情况,线程安全和重入在安全角度不做区分
但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点
可重入描述的是⼀个函数是否能被重复进⼊,表示的是函数的特点

6. 死锁

死锁是指在⼀组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会
释放的资源而处于的⼀种永久等待状态。
假如线程A,线程B必须同时持有锁1和锁2,才能进行后续资源的访问。
而现在线程A、B分别持有一把锁,就造成了两个线程互相申请对方的锁但不释放,这两个锁就成了死锁。
死锁四个必要条件
(1)互斥条件:⼀个资源每次只能被⼀个执行流使用
(2)请求与保持条件:⼀个执行流因请求资源而阻塞时,对已获得的资源保持不放
(3)不剥夺条件:⼀个执行流已获得的资源,在末使用完之前,不能强行剥夺
(4)循环等待条件:若干执行流之间形成⼀种头尾相接的循环等待资源的关系

 

 如果要避免死锁,就要破坏死锁的四个必要条件、破坏循环等待条件问题。例如资源⼀次性分配, 使用超时机制、加锁顺序⼀致。

相关文章:

  • 算法笔记·数学·最大公约数
  • 酷柚易汛ERP标签打印解决方案
  • [原创]X86C++反汇编01.IDA和提取签名
  • 为什么要使用线程池
  • 使用Python控制Arduino——入门与实战
  • IEC 60034-30-1标准解析:旋转电机能效分级与全球影响
  • LangGraph 及多agent
  • Netty学习专栏(四):如何解决粘包/拆包问题及自定义协议的实现
  • 嵌入大模型与LLM技术全面解析与实战指南
  • Day35打卡 @浙大疏锦行
  • 在Linux上安装Miniconda
  • 测试总结(一)
  • QML学习05MouseArea
  • 【LangChain大模型应用与多智能体开发 ② 接入智谱AI】
  • 【大模型面试每日一题】Day 27:自注意力机制中Q/K/V矩阵的作用与缩放因子原理
  • 搜索二叉树
  • InnoDB引擎底层解析(二)之InnoDB的Buffer Pool(三)
  • Linux驱动:基本环境准备
  • 【免费使用】剪Y专业版 8.1/CapCut 视频编辑处理,素材和滤镜
  • 基于CSP模型实现的游戏排行榜
  • 南皮县网站建设/比较成功的网络营销案例
  • 武汉大学人民医院招聘/北仑seo排名优化技术
  • 手机网站开发+图库类/如何申请百度竞价排名
  • 做网站如何对接支付/长春网站优化指导
  • 重庆市住建局官方网站/百度高级搜索引擎
  • 网站建设的资源整合与系统设计/万网域名管理入口