| |
|
开发:
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都要比传统的进程更加轻量化。透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。 线程异常:单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。 1.2 线程的优缺点优点: 缺点: 1.3 Linux进程和线程Linux中不存在真正的线程,而是用进程模拟的,Linux中所有的执行流都叫做轻量级进程。站在内核的角度,进程是分配系统资源的基本实体,线程是被系统调度和分派的实体。而这刚好对应了进程的两个特点:资源所有权和调度/执行。 进程是资源分配的基本单位。 进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境: 进程和线程的区别是什么? 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线程函数的返回值。 pthread_join 的第二个参数可以拿到等待线程的退出码,这个退出码表示着代码运行完毕,结果是否正确。但是万一线程运行出错,线程难道不会像进程一样的去处理异常吗?多线程也是需要考虑异常的,但是做不到。因为如果一个线程出现了异常,就会导致整个进程都会退出,于是代码还没有运行到 pthread_join 时,整个进程就已经退出了。 2.3 线程终止线程的终止一共有三种方案: 方案三:pthread_cancel() 一般来说, pthread_cancel() 是一个线程用来控制另外一个线程的。main thread 取消其他线程是推荐做法,新线程取消主线程是不推荐的。 2.4 线程分离是不是所有的线程都要被主线程等待呢?并不是的,默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。 可见,分离后的线程不再需要主线程的等待就可以自动被系统回收。如果主线程提前退出,那所有的线程都会退出。 最后,pthread_t 所表示的线程id,实际上是一个虚拟空间的地址。 对于多线程,系统通过LWP来识别每一个线程,但是用户层如何得知对应的线程呢?线程很多,那么也是需要被管理的:先描述再组织。但是Linux不提供真正的线程,意味着OS只需要对LWP内核执行流进行管理。那么,供用户使用的pthread接口等其他数据,应该由谁来管理呢?答案是原生线程库pthread。这些所有的接口都在库里面,原生线程库是在用户层实现的。
3.线程互斥临界资源:多线程执行流共享的资源就叫做临界资源 以一个抢票程序作为例子: 主线程创建四个线程去抢票,每个线程抢到票之后都会将票数减一,按理来说,当某一个线程取到最后一张票时,打印票数为一,然后循环就不再继续了,但是我们看到的结果是:票数打印到了-2,这明显是不合理的。 ticket-- 操作时原子操作码? 不是。其实自增和自减在汇编中是三条语句,在运行到任何一条语句时都有可能被切走。 假如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–,因此结果中出现了负数。 为了解决上述问题,需要做到以下几点: 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 可重入与线程安全线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。 常见的线程安全情况: 常见的线程不安全情况: 常见的不可重入情况: 常见的可重入情况: 可重入与线程安全联系: 可重入与线程安全区别: 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_broadcast () 唤醒了所有的线程(按顺序唤醒一个周期) 4.2 POSIX信号量POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。其实信号量本质就是一个计数器,是描述临界资源中资源数目的计数器。 线程想要访问临界资源就必须申请到信号量,就像你想去看电影就必须买电影票一样。申请到信号量的本质就是有了使用特定资源的权限,而不是已经开始使用临界资源中你申请的那个区域,就像你买了电影票并不是你不去电影院那个位子就不是你的,而是你买了电影票对应的位子也会为你留下来,你有位子的占有权。信号量存在的价值就是达到了同步互斥的效果与进行更细粒度的临界资源管理。 然而申请信号量的本质就是让计数器++,释放信号量的本质就是让计数器- -,其中申请信号量的操作叫做P操作,释放信号量的操作是V操作。本质上,信号量也是临界资源,因为要每个线程都能看到同一个信号量,所以信号量的PV操作一定要是原子的。那么定义一个全局变量count作为信号量进行++,- -是不可行的,因为本身++,- -的操作就不是原子的。 其实信号量并不是只有一个计数器而已,而是一个结构体,其中包含了对应的锁,计数器与等待队列。在进行PV操作的时候,一定存在这种情况,P操作可能会申请不到信号量,这时信号量为0,当申请不到信号量的时候,线程就只能处于阻塞状态,于是只能在等待队列里排队等候。
既然信号量是一个计数器,那么当信号量为1呢?当信号量为1,那么这就是一个二元信号量,基本就等价于互斥锁。介绍以下函数以用来模拟一个二元信号量的互斥锁。 初始化信号量: sem: 信号量变量,类型是sem_t。 销毁信号量: 等待信号量(P操作,申请信号量): 等待信号量,会将信号量的值减1。 发布信号量: 发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。 下面以抢票的例子再次以信号量互斥的方法实现: 运行结果: 4.3 生产者消费者模型下图是传统的消费者与生产者,它们彼此紧耦合,一方暂时没有需求另一方就只能阻塞,等到一方有需求了,另一方才会去做其他事情。很明显这种方法浪费了大量的时间,效率也变得很低。 但是,如果在生产者和消费者之间添加一个中间人,也就是缓冲区,生产者把生产好的数据放在缓冲区,然后生产者去做接下来要做的事情,而当消费者想要数据就不用找生产者要了,就直接可以在缓冲区里拿。这样就讲生产者与消费者进行了松耦合。这就是为什么要有生产者消费者模型的原因:对代码进行解耦。 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。 什么是生产者消费者模型: 生产者消费者模型的优点:解耦,支持并发,支持忙闲不均。 基于BlockingQueue的生产者消费者模型在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)。重要用途:实现管道。 下面是单生产者单消费者的模型代码: 在这个过程中,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.无论是生产者还是消费者,都不应该将对方绕一圈以上。 下面是单生产者单消费者的环形队列生产者消费者模型: 运行结果: 同样,我们加入多生产者多消费者,就是多生产者多消费者的环形队列的生产者消费者模型: 运行结果: 线程池而处理数据的方法提供在Task类内: main函数给ThreadPool提供Task任务: |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
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- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |