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

【Linux】--- 线程互斥

【Linux】--- 线程互斥

  • 一、线程互斥
    • 1、进程线程间的互斥相关背景概念
    • 2、互斥锁 mutex
    • 3、互斥锁原理
  • 二、常见的锁
    • 1、死锁
    • 2、自旋锁
    • 3、其他锁

一、线程互斥

1、进程线程间的互斥相关背景概念

在这里插入图片描述
进程之间如果要进行通信我们需要先创建第三方资源,让不同的进程看到同一份资源,由于这份第三方资源可以由操作系统中的不同模块提供,于是进程间通信的方式有很多种。
进程间通信中的第三方资源就叫做:临界资源,访问第三方资源的代码就叫做临界区。

例如,下面我们模拟实现一个抢票系统,我们将记录票的剩余张数的变量定义为全局变量,主线程创建四个新线程,让这四个新线程进行抢票,当票被抢完后这四个线程自动退出。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

int tickets = 1000;
void* TicketGrabbing(void* arg)
{
	const char* name = (char*)arg;
	while (1){
		if (tickets > 0){
			usleep(10000);
			printf("[%s] get a ticket, left: %d\n", name, --tickets);
		}
		else{
			break;
		}
	}
	printf("%s quit!\n", name);
	pthread_exit((void*)0);
}
int main()
{
	pthread_t t1, t2, t3, t4;
	pthread_create(&t1, NULL, TicketGrabbing, "thread 1");
	pthread_create(&t2, NULL, TicketGrabbing, "thread 2");
	pthread_create(&t3, NULL, TicketGrabbing, "thread 3");
	pthread_create(&t4, NULL, TicketGrabbing, "thread 4");
	
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_join(t3, NULL);
	pthread_join(t4, NULL);
	return 0;
}

运行结果显然不符合我们的预期,因为其中出现了剩余票数为负数的情况。
在这里插入图片描述
该代码中记录剩余票数的变量tickets就是临界资源,因为它被多个执行流同时访问,而判断tickets是否大于0、打印剩余票数以及–tickets这些代码就是临界区,因为这些代码对临界资源进行了访问。

剩余票数出现负数的原因:

  • if语句判断条件为真以后,代码可以并发的切换到其他线程。
  • usleep用于模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
  • –ticket操作本身就不是一个原子操作。

为什么- -ticket不是原子操作?

我们对一个变量进行- -,我们实际需要进行以下三个步骤:

  1. load:将共享变量tickets从内存加载到寄存器中。
  2. update:更新寄存器里面的值,执行-1操作。
  3. store:将新值从寄存器写回共享变量tickets的内存地址。

在这里插入图片描述
在这里插入图片描述

既然–操作需要三个步骤才能完成,那么就有可能当thread1刚把tickets的值读进CPU就被切走了,也就是从CPU上剥离下来,假设此时thread1读取到的值就是1000,而当thread1被切走时,寄存器中的1000叫做thread1的上下文信息,因此需要被保存起来,之后thread1就被挂起了。
在这里插入图片描述
假设此时thread2被调度了,由于thread1只进行了- -操作的第一步,因此thread2此时看到tickets的值还是1000,而系统给thread2的时间片可能较多,导致thread2一次性执行了100次- -才被切走,最终tickets由1000减到了900。
在这里插入图片描述
此时系统再把thread1恢复上来,恢复的本质就是继续执行thread1的代码,并且要将thread1曾经的硬件上下文信息恢复出来,此时寄存器当中的值是恢复出来的1000,然后thread1继续执行- -操作的第二步和第三步,最终将999写回内存。
在这里插入图片描述
在上述过程中,thread1抢了1张票,thread2抢了100张票,而此时剩余的票数却是999,也就相当于多出了100张票。

因此对一个变量进行–操作并不是原子的,虽然–tickets看起来就是一行代码,但这行代码被编译器编译后本质上是三行汇编,相反,对一个变量进行++也需要对应的三个步骤,即++操作也不是原子操作。

2、互斥锁 mutex

互斥锁是pthread库提供的,英文名为mutex(互斥),需要头文件<pthread.h>,先讲解互斥锁的基本创建和销毁方法。

互斥锁的类型是pthread_mutex_t,分为全局互斥锁 和 局部互斥锁,它们的创建方式不同。

(1)全局mutex:

想要创建一个全局的互斥锁很简单,直接定义即可:

pthread_mutex_t xxx = PTHREAD_MUTEX_INITIALIZER;

这样就创建了一个名为xxx的变量,类型是pthread_mutex_t,即这个变量是一个互斥锁,全局的互斥锁必须用宏:PTHREAD_MUTEX_INITIALIZER进行初始化!

另外,全局的互斥锁不需要手动销毁。

(2)局部mutex:

局部的互斥锁是需要通过接口来初始化与销毁的,接口如下:

①pthread_mutex_init:
pthread_mutex_init函数用于初始化一个互斥锁,函数原型如下:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

参数:

  1. restrict mutex:类型为pthread_mutex_t *的指针,指向一个互斥锁变量,对其初始化
  2. restrict attr:用于设定该互斥锁的属性,一般不用,设为空指针即可

返回值:成功返回0;失败返回错误码


②pthread_mutex_destroy:

pthread_mutex_destroy函数用于销毁一个互斥锁,函数原型如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数:类型为pthread_mutex_t *的指针,指向一个互斥锁变量,销毁该锁

返回值:成功返回0;失败返回错误码


创建好互斥锁后,就要使用这个锁,主要是两个操作:申请锁和释放锁。

三个函数的原型如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

在这里插入图片描述
这三个函数的参数都是pthread_mutex_t *mutex,即指向互斥锁变量的指针,表示要操作哪一个互斥锁。

接下来我们修改一下最初的抢票代码,给它加锁,保证抢票g_ticket–的原子性:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //全局互斥锁

void *buyTicket(void *args)
{
    customer *cust = (customer *)args;

    while (true)
    {
        pthread_mutex_lock(&mutex); // 加锁
        
        if (g_ticket > 0)
        {
            usleep(1000);
            cout << cust->_name << " get ticket: " << g_ticket << endl;
            g_ticket--;
            pthread_mutex_unlock(&mutex); // 解锁
            cust->_ticket_num++;
        }
        else
        {
            pthread_mutex_unlock(&mutex); // 解锁
            break;
        }
    }

    return nullptr;
}

1、我在此使用的是全局的互斥锁,第一行pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;就是定义了一个全局的互斥锁,并对其初始化。

2、在访问临界区前,对mutex加锁,在此我在if (g_ticket > 0)前加锁,因为不仅仅是g_ticket- -是临界区,if (g_ticket > 0)也是临界区,它们都访问了临界资源g_ticket。

3、在if的第一个分支中,当g_ticket- -完毕,此时当前线程就不会再访问g_ticket了,于是离开临界区,并对mutex解锁。在第二个分支else中,线程马上要break出循环了,并且退出,此时也要解锁,不然别的线程永远处于阻塞状态了。

4、可以想象一下,当第一个线程被调度,它要进行抢票,现在先对mutex加锁,然后再去if中访问g_ticket。假如在某个访问临界资源的过程中,CPU调度了其它线程,此时第二个线程进入。

5、第二个线程也想访问g_ticket,于是也对mutex加锁,但是由于锁已经被第一个线程申请走了,此时第二个线程pthread_mutex_lock就会失败,然后阻塞等待。

6、等到第一个线程再次被调度,访问完临界区后,对mutex解锁,此时锁又可以被申请了。

于是线程二申请到锁,再去访问g_ticket。

7、加锁可以保证,任何时候都只有一个线程访问临界区。

8、当第二个线程访问临界区时,一定是其他线程访问完毕了临界区,或者其它线程还没有访问临界区。这就保证了临界区的原子性,从而维护线程的安全!

3、互斥锁原理

加锁后的原子性体现在哪里?

引入互斥量后,当一个线程申请到锁进入临界区时,在其他线程看来该线程只有两种状态,要么没有申请锁,要么锁已经释放了,因为只有这两种状态对其他线程才是有意义的。

例如,图中线程1进入临界区后,在线程2、3、4看来,线程1要么没有申请锁,要么线程1已经将锁释放了,因为只有这两种状态对线程2、3、4才是有意义的,当线程2、3、4检测到其他状态时也就被阻塞了。
在这里插入图片描述

此时对于线程2、3、4而言,它们就认为线程1的整个操作过程是原子的。

临界区内的线程可能进行线程切换吗?

临界区内的线程完全可能进行线程切换,但即便该线程被切走,其他线程也无法进入临界区进行资源访问,因为此时该线程是拿着锁被切走的,锁没有被释放也就意味着其他线程无法申请到锁,也就无法进入临界区进行资源访问了。

其他想进入该临界区进行资源访问的线程,必须等该线程执行完临界区的代码并释放锁之后,才能申请锁,申请到锁之后才能进入临界区。

锁是否需要被保护?

我们说被多个执行流共享的资源叫做临界资源,访问临界资源的代码叫做临界区。所有的线程在进入临界区之前都必须竞争式的申请锁,因此锁也是被多个执行流共享的资源,也就是说锁本身就是临界资源。

既然锁是临界资源,那么锁就必须被保护起来,但锁本身就是用来保护临界资源的,那锁又由谁来保护的呢?

锁实际上是自己保护自己的,我们只需要保证申请锁的过程是原子的,那么锁就是安全的。

如何保证申请锁的过程是原子的?

  1. 上面我们已经说明了- -和++操作不是原子操作,可能会导致数据不一致问题。
  2. 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用就是把寄存器和内存单元的数据相交换。
  3. 由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。

在这里插入图片描述
下面我们来看看lock和unlock的伪代码:
在这里插入图片描述

我们可以认为mutex的初始值为1,al是计算机中的一个寄存器,当线程申请锁时,需要执行以下步骤:

  1. 先将al寄存器中的值清0。该动作可以被多个线程同时执行,因为每个线程都有自己的一组寄存器(上下文信息),执行该动作本质上是将自己的al寄存器清0。
  2. 然后交换al寄存器和mutex中的值。xchgb是体系结构提供的交换指令,该指令可以完成寄存器和内存单元之间数据的交换。
  3. 最后判断al寄存器中的值是否大于0。若大于0则申请锁成功,此时就可以进入临界区访问对应的临界资源;否则申请锁失败需要被挂起等待,直到锁被释放后再次竞争申请锁。

例如,此时内存中mutex的值为1,线程申请锁时先将al寄存器中的值清0,然后将al寄存器中的值与内存中mutex的值进行交换。
在这里插入图片描述
交换完成后检测该线程的al寄存器中的值为1,则该线程申请锁成功,可以进入临界区对临界资源进行访问。

而此后的线程若是再申请锁,与内存中的mutex交换得到的值就是0了,此时该线程申请锁失败,需要被挂起等待,直到锁被释放后再次竞争申请锁。
在这里插入图片描述
当线程释放锁时,需要执行以下步骤

  1. 将内存中的mutex置回1。使得下一个申请锁的线程在执行交换指令后能够得到1,形象地说就是“将锁的钥匙放回去”。
  2. 唤醒等待Mutex的线程。唤醒这些因为申请锁失败而被挂起的线程,让它们继续竞争申请锁。
    在这里插入图片描述

二、常见的锁

1、死锁

死锁:指在一组进程中的各个进程均占有不会释放的资源,但因互相申请其它进程不会释放的资源而处于的一种永久等待状态

我简单举一个例子:

现在有两个线程thread-1和thread-2,以及两把互斥锁mutex-1,mutex-2:

在这里插入图片描述
现在要求:一个线程想要访问临界资源,必须同时持有mutex-1和mutex-2。随后therad-1去申请了mutex-1,thread-2去申请了mutex-2:

在这里插入图片描述
thread-1再去申请mutex-2,结果mutex-2已经被therad-2占用了,thread-1陷入阻塞:
在这里插入图片描述
thread-2再去申请mutex-1,结果mutex-1已经被therad-1占用了,thread-2陷入阻塞:

在这里插入图片描述
现在therad-1等待therad-2解锁mutex-2,thread-2等待thread-1解锁mutex-1,双方互相等待由于唤醒thread-2需要therad-1,唤醒therad-1又需要therad-2,此时陷入永远的等待状态,这就是死锁。

想要造成死锁,有四个必要条件:

在这里插入图片描述
以上是比较正式的说法,接下来我从线程角度简单翻译翻译:
在这里插入图片描述
这四个条件都是必要条件,也就是说:

解决死锁,本质就是破坏一个或多个必要条件

在这里插入图片描述

2、自旋锁

我们先前讲的锁,其机制是这样的:
在这里插入图片描述
当线程申请一个锁失败,就会阻塞等待,当锁被使用完毕,唤醒所有等待该锁的线程。
其实锁还有一种不用阻塞等待的策略,而是反复检测的策略,就像这样:
在这里插入图片描述
当线程没有申请到锁,一段时间后再次检测这个锁有没有被释放,一直反复申请这个锁,这个过程叫做自旋。基于这个策略来申请的锁,叫做自旋锁。

Linux自带了自旋锁spinlock,类型为pthread_spinlock_t,接口如下:

创建与销毁:

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

加锁与解锁:

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

你会发现,这和mutex几乎一摸一样,所以接口也就不讲解了。

不过我这里要强调一点,pthread_spin_lock并不是申请失败就返回,而是在pthread_spin_lock内部以自旋的方式申请锁,我们无需手动模拟自旋的过程。

3、其他锁

在这里插入图片描述

相关文章:

  • 【保姆级】阿里云codeup配置Git的CI/CD步骤
  • Spring MVC 深度解析:原理、源码剖析与实战
  • Ubuntu22.04 上安装Qt5以及编译MySQL驱动
  • 从 @SpringBootApplication 出发,深度剖析 Spring Boot 自动装配原理
  • centos 9 编译安装 rtpengine (快方式)-使用 debian12 系统自带
  • SQL 基础 BETWEEN 的常见用法
  • leetcode hot100 柱状图中的最大矩形
  • Linux下S3cmd使用方式详解:从入门到高级的对象存储管理
  • RISC-V AIA学习2---IMSIC
  • macOS 安装 Miniconda
  • C++ Json-Rpc框架-3项目实现(1)(1.其它函数实现2.消息类型字段定义3.消息Message/通信Muduo抽象具象实现)
  • 深入解析 Vue3 响应式系统:原理、性能优化与应用场景
  • leetcode 2116. 判断一个括号字符串是否有效 中等
  • 如何扩展 Linux 中 ext4 文件系统的大小
  • 【PostgreSQL】pg各版本选用取舍逻辑与docker安装postgres:15
  • Day110 若依-基础
  • 微信小程序中使用WebSocket通信
  • 【2025】基于springboot+vue的高考志愿填报系统设计与实现(源码、万字文档、图文修改、调试答疑)
  • Chrome 134 版本开发者工具(DevTools)更新内容
  • WordPress分类目录绑定二级域名插件
  • 长三角哪些城市爱花钱?这个城市令人意外
  • 述评:赖清德当局上台一年恶行累累
  • 马上评|劳动课该如何找回“存在感”
  • 广东进入“倒水模式”,珠江防总、珠江委已启动Ⅳ级应急响应
  • 泽连斯基与美国副总统及国务卿会谈,讨论伊斯坦布尔谈判等问题
  • 人民日报和音:相信中国就是相信明天