并发编程
1 回顾
计算机五大组成部分:
控制器 运算器 存储器 输入设备 输出设备
CPU = 控制器 + 运算器
程序的运行过程,先将程序代码从硬盘移动到内存中,CPU再从内存中读取代码并执行。
2 多道技术
目标:让单核实现并发的效果。
2.1 并发 与 并行
并发 (concurrent) 是指多个任务轮流交替使用资源,表面上看起来具有同时处理多个任务的能力,在一段时间中看上去是在同时执行多个任务;
并行 (parallel) 是指多任务同时执行,真正拥有在同一时刻处理多个任务的能力。
并行肯定是并发,是并发的子集。
单核处理器肯定不能实现并行,但可以实现并发。
2.2 多道技术
单道也称为串行,指的是一个任务完整地运行完毕后,才可以运行下一个任务。
多道,即多路复用,是一种任务调度机制,可以节省复数个程序同时运行时的总耗时,充分利用计算机系统的资源。
多道程序系统是在计算机内存中同时存放复数个相互独立的程序,它们在管理程序(操作系统)的控制下,以交替的方式共享并争夺系统资源。
- 空间上的复用
多个程序共同使用同一套计算机硬件(例如内存空间); - 时间上的复用
切换 + 保存状态
多个程序同时共享系统资源,例如程序A占用CPU执行时,程序B可以占用IO设备进行数据输入输出操作,这样可以使系统中的各种资源尽可能地满负荷工作,从而提高整个计算机系统的使用效率。
多道技术的实现
单核CPU在任何时刻只能执行一个进程,目标是减少CPU的空闲时间。
CPU切换
- 当一个程序执行IO等操作时,操作系统会剥夺该程序对CPU的执行权;
目的:提高了CPU的利用率,并且不影响程序的执行效率。 - 当一个程序长时间占用CPU时,操作系统会剥夺该程序对CPU的执行权;
作用:使得多个程序交替运行来实现并发,但会降低了单个程序的执行效率。
3 进程
进程指的是计算机中正在执行的程序。
3.1 进程调度
进程是系统进行资源分配和调度的基本单位。
为了实现多个进程交替运行,操作系统必须对这些进程进行调度。
- 先来先服务(FCFS)调度算法
有利于长作业,而不利于短作业; - 短作业优先(SJ/PF)调度算法
对长作业不利,不能保证紧迫性作业被及时处理; - 时间片轮转法(Round Robin,RR)
让每个进程在就绪队列中的等待时间与享受服务的时间成比例。 - 多级反馈队列
设置多个就绪队列,并为各个队列赋予不同的优先级和不同的执行时间,各队列的任务优先级逐个降低,任务需要的执行时间片逐个增加;
当一个新进程进入内存后,首先将它放入第一队列的末尾,当轮到该进程执行时,如它能在该队列的设置的时间内完成,便可准备撤离,否则将该进程移入下一个队列的末尾;
只有当前队列空闲时,系统才调度执行下一个队列中的进程。
3.2 进程三状态图
- 就绪(Ready)状态
当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行; - 执行/运行(Running)状态
当进程已获得处理机,正在处理机上执行; - 阻塞(Blocked)状态
正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件包括:等待I/O操作完成、等待信件(信号)等。
3.3 重要概念
- 同步与异步
同步与异步描述的是任务的提交方式:
同步:任务提交后,等待返回结果,等待期间不做任何事情;
异步:任务提交后,不会等待返回结果,直接去做其它事情,返回结果通过异步回调机制自动处理。 - 阻塞与非阻塞
阻塞与非阻塞描述的是程序的运行状态:
阻塞:三状态图中的阻塞态;
非阻塞:三状态图中的就绪态和运行态。
最高效的组合:异步 + 非阻塞
理想状态:程序始终处在就绪态和运行态之间切换。
4 开启进程
Windows系统中创建进程必须放入main的子代码块中。
因为Windows下创建进程(进程对象.start())的过程类似于模块导入的过程,会以模块导入的方式从重新上往下依次执行代码,若创建进程的代码未放入main的子代码块中,则会导致无限死循环。
Linux中则是将代码完整地拷贝一份。
4.1 开启进程方式1
from multiprocessing import Process
import time
def task(name):
print('{name} is running'.format(name=name))
time.sleep(1)
print('{name} is over'.format(name=name))
# windows系统中创建继承必须在main的子代码块中完成。
if __name__ == '__main__':
# 1. 创建进程对象
p = Process(target=task, args=('Task1',))
# 2. 通知操作系统创建进程,异步
# 通知时代码会继续往下执行,具体完成创建进程的时间不确定。
p.start()
4.2 开启进程方式2
采用类的继承
from multiprocessing import Process
import time
class CustomProcess(Process):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print('{name} is running'.format(name=self.name))
time.sleep(1)
print('{name} is over'.format(name=self.name))
if __name__ == '__main__':
p = CustomProcess('Task1')
p.start()
4.3 总结
创建进程就是在内存中申请一片内存空间,将待执行的代码存入其中,
每一个进程对应一块独立的内存空间。
进程与进程之间默认无法之间传输数据,可以借助第三方工具实现。
4.4 join
join函数的作用是让主进程的代码等待子进程的代码执行结束后,再继续执行,不会影响其它子进程执行代码。
from multiprocessing import Process
import time
def task(name):
print('{name} is running'.format(name=name))
time.sleep(1)
print('{name} is over'.format(name=name))
if __name__ == '__main__':
p1 = Process(target=task, args=('Task1',))
p1.start()
p1.join()
4.5 对比
串行
from multiprocessing import Process
import time
def task(name, t):
print('{name} is running'.format(name=name))
time.sleep(t)
print('{name} is over'.format(name=name))
if __name__ == '__main__':
start_time = time.time()
for i in range(1, 4):
p = Process(target=task, args=(f'Task{i}', i))
p.start()
p.join()
# 当第一次执行p.join()时,代码不会继续执行,
# 等待子进程执行完毕后,再执行新一轮循环,创建下一个子进程(start())
end_time = time.time()
print(end_time - start_time) # ~6.3
并行
from multiprocessing import Process
import time
def task(name, t):
print('{name} is running'.format(name=name))
time.sleep(t)
print('{name} is over'.format(name=name))
if __name__ == '__main__':
start_time = time.time()
p_list= []
for i in range(1, 4):
p = Process(target=task, args=(f'Task{i}', i))
p.start()
p_list.append(p)
for each_p in p_list:
p.join()
# join只会阻塞主进程,不会影响其它子进程
# 第一次运行join()时,其它子进程已经开始创建(start())了。
end_time = time.time()
print(end_time - start_time) # ~3.1