IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> linux.9 Linux多线程 -> 正文阅读

[系统运维]linux.9 Linux多线程

1.Linux线程概念

1.1 什么是线程

在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。一切进程至少都有一个执行线程。线程在进程内部运行,本质是在进程地址空间内运行。在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
在这里插入图片描述

线程异常:单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
线程用途:合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。

1.2 线程的优缺点

优点:
创建一个新线程的代价要比创建一个新进程小得多。
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
线程占用的资源要比进程少很多。
能充分利用多处理器的可并行数量。
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

缺点:
性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高:编写与调试一个多线程程序比单线程程序困难得多。

1.3 Linux进程和线程

Linux中不存在真正的线程,而是用进程模拟的,Linux中所有的执行流都叫做轻量级进程。站在内核的角度,进程是分配系统资源的基本实体,线程是被系统调度和分派的实体。而这刚好对应了进程的两个特点:资源所有权和调度/执行。
既然Linux没有真正意义上的线程,所以Linux也绝对没有真正意义上的线程相关的系统调用。站在内核的角度,这里提供创建轻量级进程的接口,创建进程然后共享空间来模拟线程。站在用户的角度,我就想创建线程,可以基于轻量级进程的系统调用通过在用户层模拟实现一套线程接口 pthread 。
那又如何理解之前的进程?之前的进程只不过只有一个执行流而已。

进程是资源分配的基本单位。
线程是调度的基本单位。

线程共享进程数据,但也拥有自己的一部分数据:
在这里插入图片描述

进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
在这里插入图片描述

在这里插入图片描述

进程和线程的区别是什么?
1.根本区别:进程是资源分配的基本单位,是系统进行资源分配的一个独立单位;而线程是进程的一个实体,是CPU调度的基本单位,线程也可以看作进程内运行的一个独立程序计数器。
2.进程拥有独立的进程地址空间,每创建一个进程都会分配进程地址空间;线程之间是共享进程的地址空间的,使用相同的地址空间。
3.进程与进程之间的资源是相互独立的,互不干扰;线程与其他线程共享进程的状态和资源,所有线程都驻留在同一个地址空间中,可以访问相同的数据。
4.进程间相互独立,而一个进程内可以包含若干个线程。
5.进程的健壮性比线程好。当一个线程出现异常退出,那么该进程内的所有线程都会退出。而进程与进程之间运行互不影响。

2.Linux线程控制

在这里插入图片描述

2.1 创建线程

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

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

OS调度的时候,采用的是LWP(LIght Weight Process),而并非PID。而在Linux中,应用层的线程与内核的LWP是一对一的关系。
在这里插入图片描述

如果想要一次性创建多个线程,可以利用循环:
在这里插入图片描述

顺便调用 pthread_self() 再打印一下自己的线程id :pthread_self() 是用户级原生线程库的线程ID,LWP是内核级的。
在这里插入图片描述

2.2 线程等待

线程是需要等待的,假如其他的几个线程还在运行,但是这时主线程退出了,就会影响其他的线程,也会产生内存泄露。已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。创建新的线程不会复用刚才退出线程的地址空间。
在这里插入图片描述
在这里插入图片描述

1.如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
2.如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED ((void*)-1)。
3.如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
4.如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

pthread_join 的第二个参数可以拿到等待线程的退出码,这个退出码表示着代码运行完毕,结果是否正确。但是万一线程运行出错,线程难道不会像进程一样的去处理异常吗?多线程也是需要考虑异常的,但是做不到。因为如果一个线程出现了异常,就会导致整个进程都会退出,于是代码还没有运行到 pthread_join 时,整个进程就已经退出了。

在这里插入图片描述

2.3 线程终止

线程的终止一共有三种方案:
方案一:直接return
方案二:pthread_exit()
在这里插入图片描述

方案三:pthread_cancel()
在这里插入图片描述

在这里插入图片描述

一般来说, pthread_cancel() 是一个线程用来控制另外一个线程的。main thread 取消其他线程是推荐做法,新线程取消主线程是不推荐的。

2.4 线程分离

是不是所有的线程都要被主线程等待呢?并不是的,默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
在这里插入图片描述

可见,分离后的线程不再需要主线程的等待就可以自动被系统回收。如果主线程提前退出,那所有的线程都会退出。

最后,pthread_t 所表示的线程id,实际上是一个虚拟空间的地址。

在这里插入图片描述

对于多线程,系统通过LWP来识别每一个线程,但是用户层如何得知对应的线程呢?线程很多,那么也是需要被管理的:先描述再组织。但是Linux不提供真正的线程,意味着OS只需要对LWP内核执行流进行管理。那么,供用户使用的pthread接口等其他数据,应该由谁来管理呢?答案是原生线程库pthread。这些所有的接口都在库里面,原生线程库是在用户层实现的。
在这里插入图片描述

在这里插入图片描述
描述线程的结构就是在mmap区域中的一块一块 struct pthread,是由线程库创建并维护的。所以CPU进行调度的时候,并不会去内核里去调用内核接口完成切换,而是去到用户层动态库中去调用相关的接口对共享区内线程的结构数据进行增删更改。mmap内存映射(了解)

3.线程互斥

临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
互斥量:大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。

以一个抢票程序作为例子:
在这里插入图片描述

主线程创建四个线程去抢票,每个线程抢到票之后都会将票数减一,按理来说,当某一个线程取到最后一张票时,打印票数为一,然后循环就不再继续了,但是我们看到的结果是:票数打印到了-2,这明显是不合理的。
在这里插入图片描述

ticket-- 操作时原子操作码? 不是。其实自增和自减在汇编中是三条语句,在运行到任何一条语句时都有可能被切走。
if (ticket > 0) 是原子的吗?有没有可能其他线程正在进行?

假如thread1正在运行,这时的ticket时500,但是因为某些原因(时间片到了,该线程的优先级比较低等等)导致CPU调度了thread2,这时thread1保存其上下文数据,运行thread2。而在thread2中,其ticket自减为了300,这时thread2的时间片到了,thread1重新读取其上下文得到的ticket却还是500,这就发生了不该有的错误。再假如thread的ticket为1,进入了循环,但是刚刚进入循环就调度到了thread2,当thread2调度结束(此时ticket为0),而在thread1的上下文中ticket还是等于1,于是将ticket–,因此结果中出现了负数。

为了解决上述问题,需要做到以下几点:
代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
在这里插入图片描述

3.1 锁的初始化以及释放

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

3.2 加锁

在大部分情况下,加锁本身是有损于性能的,是几乎不可避免的,只能尽可能的减少加锁带来的性能开销成本。
进行临界资源的保护,是所有的执行流都应该遵守的标准。
在这里插入图片描述
在这里插入图片描述

加锁后的原子性体现在对于其他的线程,看到的其他线程只会处于没有申请锁与锁已经释放了的状态。那么线程在执行临界区资源的时候是否可以进行线程切换?当然可以。即便当前的线程被切走,其他线程也无法进入临界区进行访问资源,因为上一个线程是拿着锁走了,必须等上一个线程恢复执行,解锁之后,其他的线程才能进入临界区。

锁存在是为了保护临界资源,但是锁本身就是临界资源,那么谁来保护锁呢?所以申请锁的过程必须是原子的!自己来保护自己。

3.3 互斥量实现原理

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
在这里插入图片描述

首先, mutex 是一个全局变量值为 1 。在线程A执行程序时,将A的al寄存器内的值置0,然后交换寄存器与内存中的值使得A的 al 中的值为1,mutex 为0。只有当 al 中的值大于0的时候才会去访问临界资源。与此同时,线程B也在执行这样的操作,但是线程B交换B的 al 和 mutex ,始终都是 0 ,因为唯一的一份 ‘1’ 已将被A拿走了,线程B一直处于阻塞状态。如果线程A在访问临界资源之前进行了线程切换,于是线程A保存其上下文数据在A的寄存器中,然后这时线程B要将自己的 al 与 mutex进行交换,同样,它们始终为0,B也会一直阻塞。直到线程A重新被切换回来,其上下文中的 al 还是1,然后线程A继续访问刚刚未被访问的临界资源。访问完毕解锁时,将mutex 置1,使得下一个到来的线程可以具备申请锁的能力,这时B就可以去申请锁。每个线程都可以进行交换,但必须有顺序,由指令周期决定。实际上,是通过交换这一条命令完成加锁的(体现原子性) , 把共享mutex通过exchange方案原子性的交换到线程自己的上下文当中,所有的寄存器和 mutex 中只有一个 ‘1’ 。

3.4 可重入与线程安全

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

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

常见的线程安全情况:
不保护共享变量的函数。
函数状态随着被调用,状态发生变化的函数。
返回指向静态变量指针的函数。
调用线程不安全函数的函数。

常见的线程不安全情况:
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
类或者接口对于线程来说都是原子操作。
多个线程之间的切换不会导致该接口的执行结果存在二义性。

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

常见的可重入情况:
不使用全局变量或静态变量。
不使用用malloc或者new开辟出的空间。
不调用不可重入函数。
不返回静态或全局数据,所有数据都有函数的调用者提供。
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。

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

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

3.5 死锁

进程等待队列的理解:OS视角:进程线程等待某种资源,在OS层面就是将当前的进程或者线程task_struct放入对应的等待队列,状态由R->S,这种情况称之为当前进程被挂起等待了。 用户视角:用户看到的是自己的进程卡住不动了,一般称之为应用阻塞了。
在这里插入图片描述

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

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

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

避免死锁算法:
死锁检测算法-银行家算法

4.线程同步

什么是线程同步?在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免因为某个线程的竞争力太强导致其他线程申请不到锁的饥饿问题,叫做同步。 比如,今天我在教室自习,我来的最早有钥匙,于是我拿着钥匙进了教室将教室反锁,我自己就在教室自习,其他人是进不来教室的。当我自习结束,打开教室门,把钥匙挂在墙上准备走的时候,这时我又想自习了,由于这时我离钥匙最近,我的竞争力最强,所以我又拿到了钥匙,进入了教室,如此反复,其他人就没有进入教室的机会,即使教室外面排着很长的队。这就是一种饥饿现象。但是有了同步机制,这时候我出来教室就只能去排队,按照排队的顺序依次进入教室。

4.1 条件变量与条件变量函数

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。条件变量就是用来描述某种临界资源是否就绪的一种数据化描述。条件变量通常要配合mutex互斥锁一起使用。 从上面的例子来说,条件变量就是来标识墙上有没有钥匙的一个变量,在等待的人可以看到这个变量,该变量为0表示没有钥匙,该变量为1表示有钥匙。当我出来了以后,墙上挂着钥匙,这时排队的队头看见了墙上的钥匙,于是他就知道教室可以进入了。条件变量是一个全局变量,类型是 pthread_cond_t。

对于条件变量,也有对应的条件变量函数
初始化与销毁:
在这里插入图片描述

pthread_cond_init () 中,cond就是要初始化的条件变量,attr 为 nullptr。
pthread_cond_destroy () 中,cond就是要销毁的条件变量。注意这里的cond都是传的是条件变量的地址。

等待条件满足:
在这里插入图片描述
pthread_cond_wait () 中,cond传入的是条件变量的地址,当条件不满足时,该线程就在这里阻塞,当条件满足,就执行下面的代码。参数左边是条件变量地址,右边是锁地址。

唤醒等待:
在这里插入图片描述
pthread_cond_signal () 中cond传入的时条件变量的地址,唤醒等待就是将条件满足,等待队列的头一个就可以取消阻塞状态,执行下面的代码。执行完毕之后,这个线程回到队尾等待,新的队头等待被唤醒。
和 phtread_cond_signal () 不一样的是,pthread_cond_broadcast () 唤醒的是所有的线程。

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

可以观察到:当每键入一个值,都有新的线程被唤醒,一直循环,是有顺序的。这样也可以验证刚刚结束的进程被排到了队尾等待。pthread_cond_broadcast () 唤醒了所有的线程(按顺序唤醒一个周期)
在这里插入图片描述

4.2 POSIX信号量

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。其实信号量本质就是一个计数器,是描述临界资源中资源数目的计数器。 线程想要访问临界资源就必须申请到信号量,就像你想去看电影就必须买电影票一样。申请到信号量的本质就是有了使用特定资源的权限,而不是已经开始使用临界资源中你申请的那个区域,就像你买了电影票并不是你不去电影院那个位子就不是你的,而是你买了电影票对应的位子也会为你留下来,你有位子的占有权。信号量存在的价值就是达到了同步互斥的效果与进行更细粒度的临界资源管理。

然而申请信号量的本质就是让计数器++,释放信号量的本质就是让计数器- -,其中申请信号量的操作叫做P操作,释放信号量的操作是V操作。本质上,信号量也是临界资源,因为要每个线程都能看到同一个信号量,所以信号量的PV操作一定要是原子的。那么定义一个全局变量count作为信号量进行++,- -是不可行的,因为本身++,- -的操作就不是原子的。

其实信号量并不是只有一个计数器而已,而是一个结构体,其中包含了对应的锁,计数器与等待队列。在进行PV操作的时候,一定存在这种情况,P操作可能会申请不到信号量,这时信号量为0,当申请不到信号量的时候,线程就只能处于阻塞状态,于是只能在等待队列里排队等候。

struct{
    pthread_mutex_t lock;
    int count;
    task_struct* queue;
}

既然信号量是一个计数器,那么当信号量为1呢?当信号量为1,那么这就是一个二元信号量,基本就等价于互斥锁。介绍以下函数以用来模拟一个二元信号量的互斥锁。

初始化信号量:
在这里插入图片描述

sem: 信号量变量,类型是sem_t。
pshared: 0表示线程间共享,非零表示进程间共享,一般就设置为0
value:信号量初始值

销毁信号量:
在这里插入图片描述

等待信号量(P操作,申请信号量):
在这里插入图片描述

等待信号量,会将信号量的值减1。

发布信号量:
在这里插入图片描述

发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。

下面以抢票的例子再次以信号量互斥的方法实现:
在这里插入图片描述

运行结果:
在这里插入图片描述

4.3 生产者消费者模型

下图是传统的消费者与生产者,它们彼此紧耦合,一方暂时没有需求另一方就只能阻塞,等到一方有需求了,另一方才会去做其他事情。很明显这种方法浪费了大量的时间,效率也变得很低。
在这里插入图片描述

但是,如果在生产者和消费者之间添加一个中间人,也就是缓冲区,生产者把生产好的数据放在缓冲区,然后生产者去做接下来要做的事情,而当消费者想要数据就不用找生产者要了,就直接可以在缓冲区里拿。这样就讲生产者与消费者进行了松耦合。这就是为什么要有生产者消费者模型的原因:对代码进行解耦。

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
在这里插入图片描述

什么是生产者消费者模型:
三种关系:消费者和消费者(竞争关系,互斥关系),生产者和生产者(竞争关系,互斥关系),生产者和消费者(竞争关系,同步关系(保证多线程协同))。
两种角色:生产者和消费者。
一个交易场所:通常是内存中的一块缓冲区(自己通过某种方式组织起来)

生产者消费者模型的优点:解耦,支持并发,支持忙闲不均

基于BlockingQueue的生产者消费者模型

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)。重要用途:实现管道。

在这里插入图片描述

下面是单生产者单消费者的模型代码:
在这里插入图片描述
在这里插入图片描述
运行结果(当然也可以更改sleep的数值让生产者生产快一点或者消费者消费快一点):
在这里插入图片描述

在这个过程中,pthread_cond_wait()在进入等待的时候,mutex会自动释放;如果当前的等待线程被唤醒,又会自动获得相应的mutex。 但是,pthread_cond_wait()有可能被调用失败或者被伪唤醒从而出错,所以判断等待条件时 if 是不够的,要用 while 对条件进行循环检测,等到真正条件不满足时才执行 pop 或 push 的代码。
在这里插入图片描述

也可以在唤醒等待之前加上条件,满足条件之后再唤醒:当消费到数据不足容量的四分之一时,唤醒生产;当生产的数据到容量的一半时,唤醒消费。
在这里插入图片描述

当然,这里的数据不仅仅可以是一个数据,还可以是一个任务:例如一个计算任务 Task。
在这里插入图片描述
在这里插入图片描述

运行结果:
在这里插入图片描述

基于环形队列的生产消费模型

环形队列采用数组模拟,用模运算来模拟环状特性。环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态。但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程。
在这里插入图片描述

在环形队列中,既有空间也有数据,生产者关注的临界资源是有没有空间,而消费者关注的临界资源是有没有数据,于是这样我们就可以定义出两个信号量 blank_sem 和 data_sem 来分别表示临界资源中空间和数据的资源数量。一般来说,刚开始生产的时候空间全部都是空的,所以 blank_sem 是最大值,空间中都没有数据,所以 data_sem 是0。生产数据就是对某一块临界资源进行P操作,消费数据就是对某一块空间进行V操作。

环形队列生产者消费者模型应该遵守的原则:1.生产者和消费者不能指向同一个位置 2.无论是生产者还是消费者,都不应该将对方绕一圈以上。
那么在P操作和V操作同时具备条件的情况下,应该是谁先执行呢?P操作先执行。因为在刚开始时,blank_sem 不为0,而data_sem为0,所以从开始时,P操作就领先于V操作。那么有没有可能有数据不一致的问题呢?绝对不可能,导致数据不一致与临界资源竞争的问题只可能时生产者和消费者指向同一个位置时发生的,但是遍历所有的情况,只有两种情况生产者和消费者指向同一个位置,分别是队列为空和队列为满的情况,但是由于信号量的存在,很好的保证了生产者和消费者不能同时进入同一份资源,所以发生数据不一致的问题是不可能的。

下面是单生产者单消费者的环形队列生产者消费者模型:
在这里插入图片描述
在这里插入图片描述

运行结果:
在这里插入图片描述

同样,我们加入多生产者多消费者,就是多生产者多消费者的环形队列的生产者消费者模型:
在这里插入图片描述

运行结果:
在这里插入图片描述

线程池

在这里插入图片描述

而处理数据的方法提供在Task类内:
在这里插入图片描述

main函数给ThreadPool提供Task任务:
在这里插入图片描述

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-03-13 22:13:16  更:2022-03-13 22:15:56 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/16 2:48:25-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码