准备
要了解多线程和进程的概念,就得知道内核工作原理,以及并发和并行的概念。还有系统的多任务演变。
多任务系统演变
在很久以前,CPU 资源十分昂贵,如果让 CPU 只能运行一个程序,那么当 CPU 空闲下来(例如等待 I/O 时),CPU 就空闲下来了。为了让 CPU 得到更好的利用,人们编写了一个监控程序,如果发现某个程序暂时无须使用 CPU 时,监控程序就把另外的正在等待 CPU 资源的程序启动起来,以充分利用 CPU 资源。这种方法被称为 多道程序(Multiprogramming)。
对于多道程序来说,最大的问题是程序之间不区分轻重缓急,对于交互式程序来说,对于 CPU 计算时间的需求并不多,但是对于响应速度却有比较高的要求。而对于计算类程序来说则正好相反,对于响应速度要求低,但是需要长时间的 CPU 计算。想象一下我们同时在用浏览器上网和听音乐,我们希望浏览器能够快速响应,同时也希望音乐不停掉。这时候多道程序就没法达到我们的要求了。于是人们改进了多道程序,使得每个程序运行一段时间之后,都主动让出 CPU 资源,这样每个程序在一段时间内都有机会运行一小段时间。这样像浏览器这样的交互式程序就能够快速地被处理,同时计算类程序也不会受到很大影响。这种程序协作方式被称为 分时系统(Time-Sharing System)。
在分时系统的帮助下,我们可以边用浏览器边听歌了,但是如果某个程序出现了错误,导致了死循环,不仅仅是这个程序会出错,整个系统都会死机。为了避免这种情况,一个更为先进的操作系统模式被发明出来,也就是我们现在很熟悉的 多任务(Multi-tasking)系统。操作系统从最底层接管了所有硬件资源。所有的应用程序在操作系统之上以 进程(Process) 的方式运行,每个进程都有自己独立的地址空间,相互隔离。CPU 由操作系统统一进行分配。每个进程都有机会得到 CPU,同时在操作系统控制之下,如果一个进程运行超过了一定时间,就会被暂停掉,失去 CPU 资源。这样就避免了一个程序的错误导致整个系统死机。如果操作系统分配给各个进程的运行时间都很短,CPU 可以在多个进程间快速切换,就像很多进程都同时在运行的样子。几乎所有现代操作系统都是采用这样的方式支持多任务,例如 Unix,Linux,Windows 以及 macOS。
内核
一个CPU同一时刻(注意不是时间段),只能运行一个线程。而且是由内核决定运行哪个线程。也就是说多核CPU下,可以同一时刻运行的线程数受CPU数量的限制。单核CPU下经常给我们造成多个线程实际上正在同时运行的假象(其实只是CPU运作频率太高了而已)
CPU如何决定哪个线程将成为下一个运行的线程
CPU具有许多寄存器(确切的数目取决于处理器系列,例如x86与MIPS(MIPS架构 - 一种采取精简指令集的处理器架构),以及特定的家族成员,例如80486与Pentium)。当线程运行时,信息存储在那些寄存器中(例如,当前程序位置)
1
当内核决定另一个线程应该运行时,它需要:
2
1.保存当前正在运行的线程的寄存器和其他上下文信息
3
2.将新线程的寄存器和上下文加载到CPU中
优先顺序
内核将CPU分配给最高优先级的线程。优先级0为空闲线程保留-我们不能使用它,最高级别为255。
1
抢占资源(优先级较高的线程优先于优先级较低的线):
2
如果另一个具有更高优先级的线程突然能够使用 ,
3
则内核将立即上下文切换到更高优先级的线程(俗称:抢占)。
4
恢复(内核恢复运行先前的线程):
5
当较高优先级的线程完成并且内核上下文切换回之前运行的较低优先级的线程时(俗称:恢复)。
调度算法
当多个线程处于同一优先级情况下,内核将通过调度算法抉择谁优先执行
1
# 两个主要的调度算法
2
'''
3
在FIFO调度算法中,允许线程消耗CPU所需的时间。
4
这意味着,如果该线程正在进行很长的数学计算,并且没有其他优先级更高的线程就绪,
5
则该线程可能会永远运行。相同优先级的线程呢?
6
他们也被锁定。(在这一点上很明显,较低优先级的线程也被锁定。)
7
'''
8
FIFO算法策略
9
10
'''
11
RR调度算法与FIFO相同,
12
不同之处在于,如果存在另一个具有相同优先级的线程,则该线程不会永远运行。
13
它仅针对系统定义的时间片运行,时间片通常为4毫秒,但实际上是ticksize的4倍。
14
发生的情况是内核启动了RR线程并记录了时间。如果RR线程运行了一段时间,则分配给它的时间将结束(时间片将过期)。内核将查看是否有另一个具有相同优先级的线程已准备就绪。如果存在,则内核将运行它。如果不是,则内核将继续运行RR线程(即,内核授予该线程另一个时间片)
15
'''
16
Round Robin(简称RR)算法策略
17
18
19
# 系统演变(由CPU调度算法而来)
20
1.多道程序(Multiprogramming)# FIFO算法
21
2.分时系统(Time-Sharing System)# RRsuanfa
22
'''
23
避免程序错误导致死循环,操作系统从最底层接管了所有硬件资源。
24
所有的应用程序在操作系统之上以 进程(Process) 的方式运行,
25
每个进程都有自己独立的地址空间,相互隔离。
26
CPU 由操作系统统一进行分配。
27
'''
28
3.多任务(Multi-tasking)系统
CPU调度规则(对于单个CPU)
对于多CPU系统,规则相同,只是多个CPU可以同时运行多个线程
- 一次只能运行一个线程
- 优先级最高的就绪线程将运行
- 线程将一直运行直到阻塞或退出
- RR类型线程属于时间片运行,当时间过期后,会切换到其余同等级线程运行,如果没有内核将对其进行重新调度分配新的时间片运行(如果需要)
重要
要记住的重要一点是,当线程被阻塞时,无论其处于阻塞状态是什么,它都不会占用 CPU。相反,线程消耗CPU的唯一状态是RUNNING状态。
进程
一个进程可以有一个或多个线程,具有零线程的进程将无法执行任何操作,一个进程的创建,至少都有一个主线程存在。系统就是由许许多多的进程组成,每个进程负责提供某种性质的服务-无论是文件系统,显示驱动程序,数据获取模块,控制模块还是其他。
事物分解为多个进程的好处
- 解耦和模块化
- 可维护性(相互依赖很少)
- 可靠性(隔离环境,独立的内存空间,同进程下的线程将会共享这些内存空间)
进程启动方法
从命令行启动进程
比如:linux中命令启动mysql
从程序内部启动进程
比如:代码里利用函数创建启动
fork()函数
假设您要创建一个与当前正在运行的进程相同的新进程并使其同时运行,用fork()函数就可以做到,该函数复制当前进程。所有代码都是相同的,并且数据与父进程的数据相同,不过进程id是不一样的。进程之间可以通过管道、消息队列来传递。
1
# 值得一提
2
假如主线程已经用pthread_create()函数 创建另一个线程,此时是无法fork成功进程的。
3
'''
4
原因分析:
5
Neutrino C库不是为处理带有线程的进程而构建的。
6
当调用pthread_create()函数时会设置一个标志,
7
这个标志就是告诉我们说,不要让次进程fork(),
8
故意这样做的原因与线程和互斥有关,毕竟复制同一份资源容易造成资源锁问题,会比较复杂。
9
需要在多线程下fork()的话,得使用pthread_atfork()函数解决。
10
'''
11
1.fork()不适用于多个线程
vfork()函数
与fork不同,他不是复制资源,而是共享父进程的地址空间。这样的好处是进程间传输时,实现了更快,更有效的数据流。
进程间通信
分本地进程间通信方式有很多种
- 消息传递(队列、管道)
- 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
- 远程过程调用(RPC、rest api、消息队列服务器等)
线程
任何线程都可以在同一进程中创建另一个线程。没有任何限制(当然,内存空间不足!还有受内核限制了每个进程下的线程数量),同进程下的线程是共享内存空间的,并且CPU在同个进程下的线程切换上下文基本不怎么耗资源的,比较高效。
- 线程共享的环境
线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID
锁
锁要解决的是线程之间争取资源的问题
协程
协程是用户自己来编写调度逻辑的,对CPU来说,协程其实是单线程,所以CPU不用去考虑怎么调度、切换上下文,这就省去了CPU的切换开销,所以协程在一定程度上又好于多线程
- 优点
- 节省内存,因为每个线程需要分配一段栈内存,以及内核里的一些资源
- 节省分配线程的开销(创建和销毁线程要各做一次syscall)
- 节省大量线程切换带来的开销
python多线程、多进程、协程选择
python下想要充分利用多核CPU,就必须用多进程。因为每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。其他语言没有这个限制,只要是多核CPU,只要开启了多线程,就自然会利用上CPU。
CPU密集型代码(分多进程处理)
CPU密集型代码(各种循环处理、计算等等),在这种情况下,由于计算工作多,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好。
1
场景:
2
执行高密度 算法
3
方法:
4
利用多进程方式打破GIL限制,合理利用多核 资源进行算法计算。不要想着在python中用多线程计算,因为其余 即便闲置也不会去获取其余线程的任务执行(GIL限制了)
IO密集型代码(多线程或者协程)
IO密集型代码(文件处理、网络爬虫等涉及文件读写的操作),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。
1
场景:
2
web服务(内含太多网络和数据库IO读取)
3
eg:flask/django
4
方法:
5
多进程+多线程方式:
6
利用多进程打破GIL限制以及多线程遇到IO操作时自动释放 拥有权给别的线程执行。(切换线程会有点小开销)
7
多进程+多协程方式:
8
利用多进程打破GIL限制以及协程特性遇到IO操作自动模拟 切换方式跳转到其他地方执行避免不必要的等待和实际的 切换(协程在单线程下执行,无需切换 ,对IO应用很高效)