进程
理论基础
一、操作系统的作用:
- 隐藏丑陋复杂的硬件接口,提供良好的抽象接口
- 管理、调度进程,并且将多个进程对硬件的竞争变得有序
二、多道技术:
产生背景:针对单核,实现并发
ps:
现在的主机一般是多核,那么每个核都会利用多道技术
有4个cpu,运行于cpu1的某个程序遇到io阻塞,会等到io结束再重新调度,会被调度到4个
cpu中的任意一个,具体由操作系统调度算法决定。空间上的复用:如内存中同时有多道程序
- 时间上的复用:复用一个cpu的时间片
强调:遇到io切,占用cpu时间过长也切,核心在于切之前将进程的状态保存下来,这样才能保证下次切换回来时,能基于上次切走的位置继续运行
三、进程:
进程即正在执行的一个过程。进程是对正在运行程序的一个抽象。
进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一。操作系统的其他所有内容都是围绕进程的概念展开的。
PS:即使可以利用的cpu只有一个(早期的计算机确实如此),也能保证支持(伪)并发的能力。将一个单独的cpu变成多个虚拟的cpu(多道技术:时间多路复用和空间多路复用+硬件上支持隔离),没有进程的抽象,现代计算机将不复存在。- 内存越大,多个程序占用的空间越大;CPU核数越多,同一时间处理的任务越多。
什么是进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
- 进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
- 进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。[3]
进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。
进程的并行与并发
并行 : 并行是指两者同时执行,比如赛跑,两个人都在不停的往前跑;(资源够用,比如三个线程,四核的CPU )
并发 : 并发是指资源有限的情况下,两者交替轮流使用资源,比如一段路(单核CPU资源)同时只能过一个人,A走一段后,让给B,B用完继续给A ,交替使用,目的是提高效率。
区别:
并行是从微观上,也就是在一个精确的时间片刻,有不同的程序在执行,这就要求必须有多个处理器。
并发是从宏观上,在一个时间段上可以看出是同时执行的,比如一个服务器同时处理多个session。
进程的三状态

在程序运行的过程中,由于被操作系统的调度算法控制,程序会进入几个状态:就绪,运行和阻塞。
(1)就绪(Ready)状态
当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。
(2)执行/运行(Running)状态当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态。
(3)阻塞(Blocked)状态正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。

同步与异步
- 同步: 串行处理一件事物
- 异步: 同时处理多件不同的事物
- 所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。
- 所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。
阻塞与非阻塞
- 阻塞: input 读写文件 产生IO操作
- 非阻塞: 不产生IO操作
阻塞和非阻塞这两个概念与程序(线程)等待消息通知(无所谓同步或者异步)时的状态有关。也就是说阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的
同步/异步与阻塞/非阻塞
- 同步阻塞形式 效率低 专心排队,什么别的事都不做
- 异步阻塞形式 领一张排队号码,不用排队等着叫号,但是等的过程中不能做其他事情。
异步操作是可以被阻塞住的,只不过它不是在处理消息时阻塞,而是在等待消息通知时被阻塞。 - 同步非阻塞形式 一边排队,一边打电话,来回切换两种不同的行为,效率低
- 异步非阻塞形式 效率高,等着柜台(消息触发机制)通知,去外面抽烟,做自己的事
很多人会把同步和阻塞混淆,是因为很多时候同步操作会以阻塞的形式表现出来,同样的,很多人也会把异步和非阻塞混淆,因为异步操作一般都不会在真正的IO操作处被阻塞。
进程的创建与结束
但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在。而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程:
- 系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)
- 一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)
- 用户的交互式请求,而创建一个新进程(如用户双击暴风影音)
- 一个批处理作业的初始化(只在大型机的批处理系统中应用)
无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的。
进程的结束:
- 正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在linux中用exit,在windows中用ExitProcess)
- 出错退出(自愿,python a.py中a.py不存在)
- 严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以捕捉异常,try…except…)
- 被其他进程杀死(非自愿,如kill -9)
在python程序中的进程操作
创建进程共四个部分:创建进程部分,进程同步部分,进程池部分,进程之间数据共享。
multiprocess.process模块
process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建。
Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)
强调:
- 需要使用关键字的方式来指定参数
- args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号
参数介绍:
1 group参数未使用,值始终为None
2 target表示调用对象,即子进程要执行的任务
3 args表示调用对象的位置参数元组,args=(1,2,’egon’,)
4 kwargs表示调用对象的字典,kwargs={‘name’:’egon’,’age’:18}
5 name为子进程的名称
在windows中使用process模块的注意事项:必须把创建子进程的部分使用if __name__ ==‘__main__’判断保护起来
使用process模块创建进程
1 | # Process([group [, target [, name [, args [, kwargs]]]]]) |
1 | import time |
查看进程的执行
1 | import time |
1 | from multiprocessing import Process |
join方法
p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# join()
import time
from multiprocessing import Process
def func(arg1,arg2):
print('*'*arg1)
time.sleep(5)
print('*'*arg2)
if __name__ == '__main__':
p = Process(target=func, args=(10, 20))
p.start()
print('这个时候还是异步的')
p.join() # 作用:感知一个子进程的结束 ,异步的程序就变成同步了
print('主进程运行完成')
# join 会将异步的程序变成同步
开启多个子进程,并写入文件
通过join方法,让写文件的操作编程异步1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45# 多进程写文件
# for 循环500个文件,没处理1个文件需要0.1秒,500个是50秒
# 同步,只有一个进程处理: 0.1 * 500 = 50
# 异步,存在500个进程: 500 * 0.1 = 0.1
# 1. 先往文件夹中写文件
# 2. 展示写入后文件件的所有文件名
# join 会将异步的程序变成同步
import time
import os
from multiprocessing import Process
def func(filename,context):
print('子进程%s开始' %(os.getpid()))
with open(filename,'w') as f:
f.write(str(context))
# 单过在循环外只有1个join无法确定所有子进程都结束,所以需要控制
if __name__ == '__main__':
p_lst = []
for i in range(1,6):
p = Process(target=func,args=('inod%s'%i,i))
p_lst.append(p) # 每创建出来一个进程都加入进程列表
p.start()
# p.join() # 如果在循环里面join,则每循环一个进程都要等待进程的结束,会变成同步
# 如果不用join 开启进程无法保障运行的时间,所以后面的代码一起异步执行
[p.join() for p in p_lst] # 之前的所有进程必须在这里都执行完,才能执行后面的代码
# 列表推导式,先启动所有的进程,按顺序执行,在最后之前保障所有的进程对象执行完成
print('主进程%s执行完成' %(os.getpid()))
print([i for i in os.walk(r'D:\PycharmProjects\Notes\08 并发编程')])
# 场景:
# 同时开启多个子进程,异步执行,当我需要同步执行的时候,设置一个阻拦的手段,让所有的进程在这话话都执行完成
# 结果:
# 子进程8332开始
# 子进程4936开始
# 子进程2728开始
# 子进程9832开始
# 子进程4428开始
# 主进程9340执行完成
# [('inod1', 'inod2', 'inod3', 'inod4', 'inod5'])]
开启多进程方法2
1 | # 自定义类继承Process,实现多进程 |
1 | import os |
多进程之间的数据隔离
1 | # 进程 与 进程之间数据是隔离的? 是隔离的 |
守护进程
1 | # 特点: 会随着主进程的结束而结束。 |
1 | # 守护进程 |
多进程中的方法和属性
1 | import random |
socket聊天并发实例
1 | # server |
1 | # client |
进程同步 —— 锁、信号量和事件
重要程度:Lock(加锁,同一时间1个进程执行)
锁 —— multiprocess.Lock
加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:
1.效率低(共享数据基于文件,而文件是硬盘上的数据)
2.需要自己加锁处理1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43from multiprocessing import Process
from multiprocessing import Lock # 进程锁
import json
import os
import time
def show(i): # 查票读取ticket文件
with open('ticket',mode='r') as f:
dic = json.load(f)
print('子进程%s,余票:%s'%(os.getpid(),dic['ticket']))
def buy_ticket(i,lock):
lock.acquire() # 拿钥匙进门 被拿走后 别的进程会在这里阻塞,直到钥匙被还
with open('ticket') as f:
dic = json.load(f)
time.sleep(0.1)
if dic['ticket'] > 0:
dic['ticket'] -= 1
print('\033[32m%s 买到票了\033[0m' %i)
else:
print('\033[31m%s 没买到票\033[0m' %i)
time.sleep(0.1)
with open('ticket',mode='w') as f: # 修改票结果
json.dump(dic,f)
lock.release() # 还钥匙,已经买完票了
if __name__ == '__main__':
for i in range(10): # 假装有10个人抢票
p = Process(target=show,args=(i,))
p.start()
lock = Lock()
for i in range(10):
p = Process(target=buy_ticket,args=(i,lock))
p.start()
# 给某一段代码加上锁 这段代码在这一段时间内只能让一个进程执行
# 只要多人同时操作一个数据 就会出现数据安全问题,需要牺牲效率 保证数据安全
# {"ticket": 1}
信号量 —— multiprocess.Semaphore
1 | # 多进程中的组件 |
事件 —— multiprocess.Event
1 | # 通过一个信号 来控制 多个进程 同时执行或者阻塞 |
1 | # 红绿灯事件 |
进程间的通信 —— 队列和管道
进程间通信:IPC(Inter-Process Communication)
队列 multiprocessing.Queue
1 | # 队列 先进先出 |
生产者与消费者模型
1 | # 队列 |
JoinableQueue队列
1 | import time |
管道 multiprocessing.Pipe
作用:在进程之间通信
1 | from multiprocessing import Pipe,Process |
1 | # 发送多条消息 |
1 | # 作为两端通信 |
1 | import time |
进程之间的数据共享 multiprocessing.Manager
- 基于消息传递的并发编程是大势所趋
- 即便是使用线程,推荐做法也是将程序设计为大量独立的线程集合,通过消息队列交换数据。
- 这样极大地减少了对使用锁定和其他同步手段的需求,还可以扩展到分布式系统中。
- 但进程间应该尽量避免通信,即便需要通信,也应该选择进程安全的工具来避免加锁带来的问题。
- 以后我们会尝试使用数据库来解决现在进程之间的数据共享问题。
- 进程间数据是独立的,可以借助于队列或管道实现通信,二者都是基于消息传递的
- 虽然进程间数据独立,但可以通过Manager实现数据共享,事实上Manager的功能远不止于此
1 | # 进程之间的数据共享 |
1 | def func(dic,lock): |
进程池 Pool
multiprocessing.Pool
为什么要有进程池?进程池的概念。
- 创建进程需要消耗时间,销毁进程也需要消耗时间。不能无限制的根据任务开启或者结束进程。
- 进程池的概念:定义一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务,等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。
Pool([numprocess [,initializer [, initargs]]]):创建进程池1
2
3
4# 参数介绍:
# 1 numprocess:要创建的进程数,如果省略,将默认使用cpu_count()的值
# 2 initializer:是每个工作进程启动时要执行的可调用对象,默认为None
# 3 initargs:是要传给initializer的参数组
map()方法 进程池和进程效率测试
1 | # 为什么会有进程池的概念 |
1 | # 使用进程池 |
进程池中的同步和异步调用
1 | # 进程池的同步调用 |
1 | # 使用进程池创建socket_server |
进程池的返回值
1 | import requests |
1 | # 进程池的返回值 |
1 | # map |
回调函数 callback
1 | import os |
回调函数 – 爬虫
1 | import requests |
