| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 系统运维 -> 【Linux】多线程(一) -> 正文阅读 |
|
[系统运维]【Linux】多线程(一) |
今天我们来讲一下多线程。 1.Linux 线程概念1.1 背景引入相信大家如果学习过Linux下的进程的话,下面这种图大家都很熟悉了: 如果我们今天创建进程,不独立创建地址空间,用户级页表,甚至不进行IO将程序数据和代码加载到内存。 这样的操作,是的每一个 task_struct 都可以使用进程的一部分资源。此时我们的每一个PCB被CPU调度的时候,执行的“粒度”都要比原始进程执行的“粒度”更小一点。 1.2 线程的概念线程本质是在进程的地址空间内运行的一个执行流。 1.3 重新理解进程我们将橙色区域统一称为 Linux进程. 站在OS的角度,进程是承担分配系统资源的基本单位。一个进程被创建之后,后续可能会存在多个执行流(线程). 那么又如何看待我们之前学习的,使用的进程? 其本质是承担系统资源的实体,不过内部只存在一个执行流。 1.4 Linux 线程 vs 其他平台下的线程站在CPU的角度,不存在任何区别。不管是我们过去所理解的进程,还是现在的进程,在CPU看来都是与PCB打交道。但是CPU执行的时候,可能执行的“进程流”已经比执行之前更加轻量化了。 但是,其实在Linux下,是没有真正意义上的线程的概念的,而是使用进程来模拟的。此时我们将这类“进程”叫做“轻量级进程”。 但是,Windows中是具有真正的线程概念的。 系统内可能存在大量的进程,进程:线程=1:n ,那么系统中一定存在着大量的线程。这也大致OS要进程线程的管理(先描述,再组织) 。所以,支持真线程的系统是一定要做到描述线程的TCB(thread ctrl block)。 此时这样的系统既需要线程管理(TCB),又需要进程管理(PCB),这样的设计会导致高复杂性,低可依赖性,所以从这方面考虑,Linux 的设计优于Windows. 1.5 进程 vs 线程进程是承担分配系统资源的基本实体,线程是OS调度的基本单位。 同一进程的线程共享 同一地址空间吗,因此 TextSegment ,DataSegment都是可以共享的,如果定义一个函数,在各个线程中都可以调用,如果定义一个全局变量,在各个线程中都可以访问,除此之外,各线程中都可以访问到。除此以外,各线程还共享以下进程资源和环境:
不过,线程也还是拥有自己的一部分数据的:
1.6 线程的操作对于线程的操作,由于Linux 不存在真正的线程,所以也不可能直接在OS层面上提供系统调用接口,最多是轻量级进程的调度接口。 我们一般使用原始线程库,这是Linux 在应用层封装的一套对外接口。 1.7 线程的优缺点1.7.1 线程的优点
1.7.2 线程的缺点因为所有的PCB都是共享地址空间,理论上,每一个“线程”都可以看到进程的所有资源。这样的好处是线程间通信的成本很低,但是缺点也很明显:一定存在大量的临界资源,这也势必可能需要使用各种互斥和同步机制来保证资源安全。
1.8 线程异常线程 是进程的一个执行分支,野指针,除0等异常操作导致线程退出的同时,也意味着进程发生了该错误,进程也会随之崩溃退出。 2. 线程控制2.1 POSIX 线程库POSIX 线程库是系统提供的基于应用层的一套线程库。
2.2 创建线程功能:创建一个线程 函数原型:
参数:
返回值: 成功返回0,失败返回错误码 我们写一段代码: 阅读上面的代码,我们存在两个线程,一个在主函数之中,一个在thread_run之中。 我们使用 ps 命令来查看进程,发现只有一个进程在运行:
所以CPU在调度多执行流进程的时候,是依据LWP来区分各个执行流。 这时候有同学就会有疑问了,那么我们代码中的tid是啥?又有什么用途? 观察tid的值以及其地址,我们可以推测出tid有可能是一种地址数据,之后我们再进一步介绍。 2.3 进程退出如果需要只终止某个线程而不是进程,可以有三个看法:
pthread_exit函数 功能:线程终止
参数:value_ptr:value_ptr不要指向一个局部变量 2.4 获取进程idpthread_self() 使用很简单,我们在那个线程下使用,调用该函数,就可以得到其线程id. 2.5 线程取消pthread_cancel() 功能:取消一个执行中的线程
参数: 常规情况下我们不建议在子线程中取消主线程。 2.6 线程等待线程在终止之后,一般要进行等待,主线程如果不等待,会造成和进程退出类似的效果(僵尸进程) pthread_join() 功能: 等待线程结束
参数:
返回值: 成功返回0,失败返回错误码 为什么需要线程等待?
我们可以写一个程序来测试一下: 那么对于线程来说呢?也是类似的,但是线程接口更加灵活,返回类型是 void*类型,也就是说,我们可以返回任何自定义类型作为返回值。 但是,想要接受这个值并不是拿一个同类型的变量去接收,而是用pthread_join中的第二个(输出型)参数去接受 我们不但可以退出 数值,还可以退出结构体等自定义类型,比如下列代码: 我们在上面讨论的都是正常的情况,如果发生异常,又存在哪些情形呢? 已知进程退出有几种可能:
那么我们的主线程在进行进程等待的时候,需不需要考虑线程崩溃的问题? 并不需要,因所有线程是一个“命运共同体”,线程出现错误直接导致进程退出,所以最后还是依靠父进程通过退出码/信号 来判断进程的退出原因。 我们再来进行一个测试,我们启动五个子线程,然后立即取消三个线程,观察会发生什么:
2.7 在线程中进行程序替换我们是不建议在进程中进行程序替换的,因为程序替换对应的是 进程级别,我们在一个线程中替换了代码,那么由于各个线程共享进程空间,一边则都变。可能造成不好的结果。 2.8 线程分离默认的情况下,新创建的线程是joinbale,线程退出后之后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄露。 但是不关心线程的返回值,join是一种,这个时候,我们可以告诉系统,当线程退出的时候,自动释放线程资源。 分离的本质,是让主线程不用再join 新线程,从而可以让新线程退出的时候,自动回收资源! pthread_detach(pthread_t thread) 眼见为实,我们写一个程序: 我们在一个新线程中把自己分离,但是依然在主线程中去等待它,观察结果: 如果一个线程被设置为分离状态,该线程不应该被join,结果是未定义的,一定会join出错,join的出错,也导致了return 0直接执行,进程被释放,剩下的4个线程无法执行。 更加稳妥的方式是在主线程中完成子线程的分离(不用再先sleep了),但是,无论何时都不用再等待了。 但是,即便线程被设置为分离状态,但是如果该线程出错奔溃(除零,野指针…),还是会影响主线程和其他的正常线程。 2.9 进一步了解线程库NPTLa. 原生线程库是一个库
所谓的动态库 就是pthread库,在这其中存在许多的小的数据段用来维护 线程在用户级别下的相关数据(比如线程PCB,上下文数据,线程栈等),其中所谓的tid 是每一个效数据段的起始地址,帮助我们找到每一个线程。 对于我们创建每一个 用户及线程,在底层都会对应一个(或多个)LWP(内核级线程),真正执行操作的使内核级操作。 我们可以把内核空间理解为 黑社会,LWP是卧底,用户级线程是警察,他们是一对一对接,警察可以要求卧底去完成任务,并查看是否完成。 3. Linux线程互斥3.1 问题引入我们按照之前学习的的多线程,写一个简单的抢票程序: 假设公有2000张票,我们建五个线程去抢票,平打印票数的变化过程。 我们惊奇的发现,最后票数居然变成了一个负数。 问题在于 当多线程对一个全局变量(临界资源),进行一一操作的时候,是否是原子的呢?显然并不是。 我们可以想一下,CPU计算 ticket–这个语句,需要有几步?
如果一个操作是由大于1句构成的,绝对不是原子的。 如果我们现在有线程A和B: A线程先运行,但是在运行完第2步之后时间片到了,线程A先关数据被剥离CPU,上下文数据暂时存储在线程A的PCB中,也就是 999作为一个临时量存储起来,等待下次时间片到来时执行第3步。线程A剥离,线程B开始执行,在一个时间片的时间内,线程B完整的执行了10次 ticket–的操作,此时内存中的ticket值为990,此时线程B剥离,线程A执行,A将上下文数据载入CPU,cpu执行第三部将999写会内存,此时 ticket由 990 变成了 999 ,反而增加了。 所以,在多线程切换的情况下,极有可能出现因为数据交叉操作,而导致的数据不一致问题。 3.2 互斥量互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
要解决以上问题,需要做到三点:
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量 3.3 互斥量的接口3.3.1 初始化互斥量初始化互斥量有两种方法:
原型:
参数:
3.3.2 销毁互斥量声明:
销毁互斥量需要注意:
3.3.3 互斥量加锁与解锁声明:
返回值: 成功返回0,失败返回错误码 这里我们要注意:
那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁 由此,我们可以完善我们的抢票程序,不过相应的,运行速度也会变慢: 这里我们需要强调几点:
2.加锁不会使线程对应时间片延长,线程依旧随时会被切走。虽然我们随时可以被切走,但是我们是拿着唯一的一把锁走的,也就是,虽然我走了,但是别人也无可奈何,只能对着“数据”干瞪眼,最后时间到了,我回来了,我就继续执行,知道循环反复我把事情做完,再把锁释放。而且,任何人在我不在的时候,是不可能申请到锁的。 对于其他的线程来说,用有锁的线程执行自己的临界区命令的时候,要么不执行要么执行完毕。这也间接的实现了 原子性。 3.4 可重入VS线程安全3.4.1 概念引入
3.4.2 常见的线程不安全情况
3.4.3 常见的线程安全的情况
3.4.4 常见的不可重入的情况
3.4.5 常见的可重入的情况
3.4.6 可重入与线程安全的联系与差别
3.5 常见锁概念3.5.1 死锁死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。 死锁的四个必要条件
避免死锁
避免死锁的算法(了解)
4.Linux 线程同步4.1 条件变量条件变量(cond)是一个由线程库提供的描述临界资源状态的对象的变量。 通过条件变量,不需要再频繁的通过申请或者释放锁的方式,也能够达到检测临界资源的目的。 为什么我们需要条件变量呢? 如果没有条件变量,我们的线程由于不知道资源的情况,只能不断通过轮询访问的方式不断的去申请,检测。 打个比方:你十分的饥饿,点了外卖,但是不知道外卖的状态,你就每隔20秒就打一次商家和骑手的电话,询问外卖的状态,这就是所谓的“轮询”,这显然不是一种明智的手段。 4.2 同步概念与竞态条件
所谓的饥饿问题,通俗来说,就是存在一个几个竞争能力特别的强的线程,每一次申请,都是它申请到锁,该线程一直进行申请,检测,释放锁,导致其他线程没有机会用到锁,这导致线程的“饥饿”。 4.3 条件变量的接口4.3.1 条件变量的初始化4.3.2 条件变量的销毁4.3.3 唤醒等待作用:唤醒在指定条件变量下等待的一个或多个线程 函数原型:
其中 pthread_cond_signal 是唤醒一个等待中线程,pthread_cond_broadcast类似与一个广播,唤醒一个等待队列中的所有的等待线程。 4.3.4 进行等待作用: 让线程在指定的条件下进行等待 (直白点就是,没人叫你就先去等着) 4.3.5 简单的实际运用我们通过上面的接口可以实现用一个线程来控制另一个线程。 代码如下:我们让线程2来控制线程1,线程2每隔一秒唤醒一次线程 我们也可以验证之前所说的 cond 下的等待队列: 可以发现,thread1,2,3 这三个线程按照顺序被激活,也就是按找顺序来访问临界资源: 这就是我们多线程的第一部分的内容。 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
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/15 20:05:39- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |