并发编程简介
2.并发编程
1) 操作系统发展史
1.1 穿孔卡片 -
- 读取数据速度特别慢
- CPU的利用率极低
- 单用户(一份代码)使用
1.2 批处理
- 读取数据速度特别慢
- CPU的利用率极低
- 联机(多份代码)使用
- 效率还是很低
1.3 脱机批处理(现代操作系统的设计原理)
- 读取数据速度提高
- CPU的利用率提高
- 每次运行期间发出I/O请求后后,高速 的cpu便会处于空闲
2)多道技术(基于单核背景下产生的)
单道:(串行)
- 比如: a,b程序同时需要使用cpu,会让a程序先使用,b等待a使用完毕后,b才能使用cpu。
多道:
- 比如: a,b程序需要使用cpu,a先使用,b等待a,直到a进入“IO或执行时间过长” a会(切换 + 保存状态),然后b可以使用cpu,待b执行遇到 “IO或执行时间过长” 再将cpu执行权限交给a,直到两个程序结束。
多路复用:
# 空间上的复用(*******):多个程序使用一个CPU。
将内存分为几部分,每个部分放入一个程序,这样,同一时间内存中就有了多道程序
'''
空间上的复用最大的问题是:程序之间的内存必须分割,这种分割需要在硬件层面实现,由操作系统控制。如果内存彼此不分割,则一个程序可以访问另外一个程序的内存,
首先丧失的是安全性,比如你的qq程序可以访问操作系统的内存,这意味着你的qq可以拿到操作系统的所有权限。
其次丧失的是稳定性,某个程序崩溃时有可能把别的程序的内存也给回收了,比方说把操作系统的内存给回收了,则操作系统崩溃。
'''
时间上的复用(*******):即切换 + 保存状态
当一个程序在等待I/O时,另一个程序可以使用cpu,如果内存中可以同时存放足够多的作业,则cpu的利用率可以接近100%,类似于我们小学数学所学的统筹方法。(操作系统采用了多道技术后,可以控制进程的切换,或者说进程之间去争抢cpu的执行权限。这种切换不仅会在一个进程遇到io时进行,一个进程占用cpu时间过长也会切换,或者说被操作系统夺走cpu的执行权限)
1) 当执行程序遇到IO时,操作系统会将CPU的执行权限剥夺。
优点: CPU的执行效率提高
2) 当执行程序执行时间过长时,操作系统会将CPU的执行权限剥夺。
缺点: 程序的执行效率低
'''
特点:
(1)多路性。若干个用户同时使用一台计算机。微观上看是各用户轮流使用计算机;宏观上看是各用户并行工作。
(2)交互性。用户可根据系统对请求的响应结果,进一步向系统提出新的请求。这种能使用户与系统进行人机对话的工作方式,明显地有别于批处理系统,因而,分时系统又被称为交互式系统。
(3)独立性。用户之间可以相互独立操作,互不干扰。系统保证各用户程序运行的完整性,不会发生相互混淆或破坏现象。
(4)及时性。系统可对用户的输入及时作出响应。分时系统性能的主要指标之一是响应时间,它是指:从终端发出命令到系统予以应答所需的时间。
'''
并发与并行:(*******)
并发: 在单核(一个cpu)情况下,当执行两个a,b程序时,a先执行,当a遇到IO时,b开始争抢cpu的执行权限,再让b执行,他们看起像同时运行。
并行: 在多核(多个cpu)的情况下,当执行两个a,b程序时,a与b同时执行。他们是真正意义上的同时运行。
面试题: 在单核情况下能否实现并行? 不行
3) 进程
1.什么是进程?
正在进行的一个过程或者说一个任务。是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个抽象概念,所有多道程序设计操作系统都建立在进程的基础上。
2.进程与程序:
- 程序: 一对代码文件。
- 进程: 执行代码的过程,称之为进程。
# 举个例子
想象一位tom猫正在给他的宿敌jerry做奶油蛋糕。
他有做奶油蛋糕的食谱及所需的所有原料:面粉、鸡蛋、奶油等。
在这个比喻中:
做蛋糕的食谱就是程序
tom猫就是处理器(cpu)
而做蛋糕的各种原料就是输入数据
进程就是tom阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和
现在假设tom猫的主人站在门外需要tom去开门
tom想了想,给主人开门的任务比给jerry鼠做蛋糕的任务更重要,于是tom立马先记下照着食谱做到第几步(保存进程的当前状态),然后跑去给主人开门。这里,我们看到处理机从一个进程(做蛋糕)切换到另一个高优先级的进程(开门),每个进程拥有各自的程序(食谱和开锁)。开门之后,tom又回来从他离开时的那一步继续做蛋糕
需要强调的是:同一个程序执行两次,那也是两个进程,比如打开快播,虽然都是同一个软件,但是一个可以播放苍井空,一个可以播放饭岛爱,分别做不同的事情也不会混乱
3 进程调度: (了解)
-
先来先服务调度算法(了解)
比如程序 a,b,若a先来,则让a先服务,待a服务完毕后,b再服务。
缺点: 执行效率低。
-
短作业优先调度算法(了解)
执行时间越短,则先先调度。
缺点: 导致执行时间长的程序,需要等待所有时间短的程序执行完毕后,才能执行。
现代操作系统的进程调度算法: 时间片轮转法 + 多级反馈队列 (知道)
-
时间片轮转法
时间片轮转(Round Robin,RR)法的基本思路是让每个进程在就绪队列中的等待时间与享受服务的时间成比例。在时间片轮转法中,需要将CPU的处理时间分成固定大小的时间片,例如,几十毫秒至几百毫秒。如果一个进程在被调度选中之后用完了系统规定的时间片,但又未完成要求的任务,则它自行释放自己所占有的CPU而排到就绪队列的末尾,等待下一次调度。同时,进程调度程序又去调度当前就绪队列中的第一个进程。
比如同时有10个程序需要执行,操作系统会给你10秒,然后时间片轮转法会将10秒分成10等分。
- 多级反馈队列:
1级队列: 优先级最高,先执行次队列中程序。
2级队列: 优先级以此类推
3级队列:
-
前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。 而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。 (1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。 (2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。 (3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。
4) 同步与异步(*******):
同步与异步 指的是 “提交任务的方式”。
同步(串行): 两个a,b程序都要提交并执行,假如a先提交执行,b必须等a执行完毕后,才能提交任务。
一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。
异步(并发): 两个a,b程序都要提交并执行,假如a先提交并执行,b无需等a执行完毕,就可以直接提交任务。
不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。
5)阻塞与非阻塞(*******):
-
阻塞(等待): 凡是遇到IO都会阻塞。
IO: input /output , time.sleep(3) , 文件的读写 , 数据的传输
- 非阻塞 (不等待) : 除了IO都是非阻塞 (比如: 从1+1开始计算到100万这种计算过程不是阻塞)
6) 进程的三种状态(*******):
- 就绪态: 同步与异步
- 运行态: 程序的执行时间过长 ----> 将程序返回给就绪态(这是非阻塞)
- 阻塞态: 遇到IO
面试题: 阻塞与同步是一样的吗?非阻塞与异步是一样的吗?
答:
IO密集型:这时阻塞与同步是不一样的,
计算密集型:这是阻塞与同步看起来一样,但是本质上仍然不一样
同步与异步:提交任务的方式
阻塞与非阻塞: 进程的状态。
异步非阻塞: ----> cpu的利用率最大化!
7) 创建进程的方式:
但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如扫地机器人,一旦启动,所有的进程都已经存在。
而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程:
1. 系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)
2. 一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)
3. 用户的交互式请求,而创建一个新进程(如用户用鼠标双击任意一款软件图片:qq,微信,暴风影音等)
4. 一个批处理作业的初始化(只在大型机的批处理系统中应用)
无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的。
multiprocess模块
仔细说来,multiprocess不是一个模块而是python中一个操作、管理进程的包。 之所以叫multi是取自multiple的多功能的意思,在这个包中几乎包含了和进程有关的所有子模块。由于提供的子模块非常多,为了方便大家归类记忆,我将这部分大致分为四个部分:创建进程部分,进程同步部分,进程池部分,进程之间数据共享。
Process类创建进程的两种方式
from multiprocessing import Process
import time
# 方式1: 直接调用Process
def task(name): # 任务 # name == 'jason_sb'
print(f'start...{name}的子进程')
time.sleep(3)
print(f'end...{name}的子进程')
if __name__ == '__main__':
# target=任务(函数地址) ---> 创建一个子进程
# 异步提交了三个任务
p_obj1 = Process(target=task, args=('jason_sb', ))
p_obj1.start() # 告诉操作系统,去创建一个子进程
p_obj1.join() # 告诉主进程,等待子进程结束后,再结束
p_obj2 = Process(target=task, args=('sean_sb', ))
p_obj2.start() # 告诉操作系统,去创建一个子进程
p_obj2.join() # 告诉主进程,等待子进程结束后,再结束
p_obj3 = Process(target=task, args=('大饼_sb', ))
p_obj3.start() # 告诉操作系统,去创建一个子进程
p_obj3.join() # 告诉主进程,等待子进程结束后,再结束
print('正在执行当前主进程...')
list1 = []
for line in range(10):
p_obj = Process(target=task, args=('jason_sb',))
p_obj.start()
list1.append(p_obj)
for obj in list1:
obj.join()
print('主进程')
# 方式二:
from multiprocessing import Process
import time
class MyProcess(Process):
def run(self):
print(f'start...{self.name}的子进程')
time.sleep(3)
print(f'end...{self.name}的子进程')
if __name__ == '__main__':
list1 = []
for line in range(10):
obj = MyProcess()
obj.start()
list1.append(obj)
for obj in list1:
obj.join()
print('主进程...')
'''
强调:在Windows操作系统中由于没有fork(linux操作系统中创建进程的机制),在创建子进程的时候会自动 import 启动它的这个文件,而在 import 的时候又执行了整个文件。因此如果将process()直接写在文件中就会无限递归创建子进程报错。所以必须把创建子进程的部分使用if __name ==‘__main’ 判断保护起来,import 的时候 ,就不会递归运行了。
'''
8)回收进程的两种方式
- join让主进程等待子进程结束,并回收子进程资源,主进程再结束并回收资源。
# 第一种
from multiprocessing import Process
import time
#任务
def task():
print('start...')
time.sleep(2)
print('end...')
if __name__ == '__main__':
p = Process(target=task)
# 告诉操作系统帮你开启一个新的进程
p.start()
# 告诉操作系统等待子进程结束
p.join()
time.sleep(1)
print('主进程结束')
- 主进程 “正常结束” ,子进程与主进程一并被回收资源。
# 第二种
'''
主进程 “正常结束” ,子进程与主进程一并被回收资源。
'''
from multiprocessing import Process
import time
#任务
def task():
print('start...')
time.sleep(1)
print('end...')
if __name__ == '__main__':
p = Process(target=task)
# 告诉操作系统帮你开启一个新的进程
p.start()
time.sleep(2)
print('主进程结束')
9)僵尸进程与孤儿进程(了解)
-
僵尸进程 (有坏处):
在子进程结束后,主进程没有正常结束, 子进程PID不会被回收。 缺点: 操作系统中的PID号是有限的,如有子进程PID号无法正常回收,则会占用PID号,造成资源浪费,若PID号满了,则无法创建新的进程。
词语解释:PID
进程pID(英语:processID)、PID)是大多数操作系统的内核用于唯一标识进程的一个数值。(简言之,就是进程的绰号。)这一数值可以作为许多函数调用的参数,以使调整进程优先级、kill(命令)进程之类的进程控制行为成为可能。
-
孤儿进程(没有坏处):
在子进程没有结束时,主进程没有“正常结束”, 子进程PID不会被回收。
操作系统优化机制(孤儿院): 当主进程意外终止,操作系统会检测是否有正在运行的子进程,会他们放入孤儿院中,让操作系统帮你自动回收。
'''
僵尸进程
'''
from multiprocessing import Process
# 在子进程中调用可以拿到子进程对象,.pid可以获取pid号
# 在主进程中调用可以拿到主进程对象,.pid可以获取pid号
from multiprocessing import current_process
import os
# current_porcess模块中的current_process().pid和os模块的os.getpid()
# 都可以获取子进程和主进程的pid号
import time
# 任务
def task():
print(f'start...{current_process().pid}')
time.sleep(3)
print(f'end.....{current_process().pid}')
print('子进程结束了')
if __name__ == '__main__':
p = Process(target=task)
p.start()
print(f'进入主进程的IO--->{current_process().pid}')
print(f'进入主进程的IO--->{os.getpid()}')
# 主进程结束
print('主进程结束...')
print(f'查看主进程{os.getpid()}')
# 为了让主程序异常报错结束
f = open('tank.txt')
print(f'查看主进程{os.getpid()}')
10)守护进程
当主进程结束时,子进程也必须结束,并回收。
主进程创建守护进程
其一:守护进程会在主进程代码执行结束后就终止
其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children
注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止
import os
import time
from multiprocessing import Process
class Myprocess(Process):
def __init__(self,person):
super().__init__()
self.person = person
def run(self):
print(os.getpid(),self.name)
print('%s正在和女主播聊天' %self.person)
p=Myprocess('炮王')
p.daemon=True # 一定要在p.start()前设置,设置p为守护进程,禁止p创建子进程,并且父进程代码执行结束,p即终止运行
p.start()
time.sleep(10) # 在sleep时查看进程id对应的进程ps -ef|grep id
print('主')
# 一个迷惑性的例子
from multiprocessing import Process
def foo():
print(123)
time.sleep(1)
print("end123")
def bar():
print(456)
time.sleep(3)
print("end456")
p1=Process(target=foo)
p2=Process(target=bar)
p1.daemon=True
p1.start()
p2.start()
time.sleep(0.1)
print("main-------")#打印该行则主进程代码结束,则守护进程p1应该被终止.#可能会有p1任务执行的打印信息123,因为主进程打印main----时,p1也执行了,但是随即被终止.
11)进程间数据隔离
进程隔离是为保护操作系统中进程互不干扰而设计的一组不同硬件和软件的技术
这个技术是为了避免进程A写入进程B的情况发生。 进程的隔离实现,使用了虚拟地址空间。进程A的虚拟地址和进程B的虚拟地址不同,这样就防止进程A将数据信息写入进程B
进程隔离的安全性通过禁止进程间内存的访问可以方便实现
from multiprocessing import Process
n=100
def work():
global n
n=0
print('子进程内: ',n)
if __name__ == '__main__':
p=Process(target=work)
p.start()
print('主进程内: ',n)
12)进程互斥锁(multiprocess.Lock)
进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的,
而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理
#文件db的内容为:{"count":1}
#注意一定要用双引号,不然json无法识别
from multiprocessing import Process,Lock
import time,json,random
def search():
dic=json.load(open('db'))
print('\033[43m剩余票数%s\033[0m' %dic['count'])
def get():
dic=json.load(open('db'))
time.sleep(0.1) #模拟读数据的网络延迟
if dic['count'] >0:
dic['count']-=1
time.sleep(0.2) #模拟写数据的网络延迟
json.dump(dic,open('db','w'))
print('\033[43m购票成功\033[0m')
def task():
search()
get()
if __name__ == '__main__':
for i in range(100): #模拟并发100个客户端抢票
p=Process(target=task)
p.start()
# 引发问题:数据写入错乱
互斥锁保证数据安全
from multiprocessing import Process,Lock
import time,json,random
def search():
dic=json.load(open('db'))
print('\033[43m剩余票数%s\033[0m' %dic['count'])
def get():
dic=json.load(open('db'))
time.sleep(random.random()) # 模拟读数据的网络延迟
if dic['count'] >0:
dic['count']-=1
time.sleep(random.random()) # 模拟写数据的网络延迟
json.dump(dic,open('db','w'))
print('\033[32m购票成功\033[0m')
else:
print('\033[31m购票失败\033[0m')
def task(lock):
search()
lock.acquire() # 将买票这一环节由并发变成了串行,牺牲了运行效率但是保证了数据的安全
get()
lock.release()
if __name__ == '__main__':
lock = Lock()
for i in range(100): # 模拟并发100个客户端抢票
p=Process(target=task,args=(lock,))
p.start()
总结:加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,
但牺牲了速度却保证了数据安全。
问题:虽然可以用文件共享数据显示进程间数据通信但问题是
效率低(共享数据基于文件,而文件是硬盘上的数据)
需要自己加锁处理
13)队列:
FIFO:先进先出简写,指的就是队列
FILO:先进后出,指的就是堆栈
先进先出 进----》 [3, 2, 1] ----》 出 1, 2, 3 ,先存放的数据,就先取出来。
相当于一个第三方的管道,可以存放数据。
应用: 让进程之间数据进行交互。
基本用法
Queue([maxsize]) # 创建共享的进程队列 队列底层使用管道和锁定实现。
# 参数 :maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。
代码实现
from multiprocessing import Queue
q=Queue(3) # 创建一个最大只能容纳3个数据的队列
"""
常用方法
put ,get ,put_nowait,get_nowait,full,empty
"""
q.put(3) # 往队列中存放数据
q.put(3)
q.put(3)
q.put(3) # 如果队列已经满了,程序就会停在这里,等待数据被别人取走,再将数据放入队列。如果队列中的数据一直不被取走,程序就会永远停在这里。
try:
q.put_nowait(3) # 可以使用put_nowait,如果队列满了不会阻塞,但是会因为队列满了而报错。
except: # 因此我们可以用一个try语句来处理这个错误。这样程序不会一直阻塞下去,但是会丢掉这个消息。
print('队列已经满了')
# 因此,我们再放入数据之前,可以先看一下队列的状态,如果已经满了,就不继续put了。
print(q.full()) # 判断队列中数据是否已存放满了
print(q.get()) # 从队列中获取数据
print(q.get())
print(q.get())
print(q.get()) # 同put方法一样,如果队列已经空了,那么继续取就会出现阻塞。
try:
q.get_nowait(3) # 可以使用get_nowait,如果队列满了不会阻塞,但是会因为没取到值而报错。
except: # 因此我们可以用一个try语句来处理这个错误。这样程序不会一直阻塞下去。
print('队列已经空了')
print(q.empty()) # 判断队列中数据是否已经被全部取出
基于队列实现进程间通信
import time
from multiprocessing import Process, Queue
def f(q):
q.put('hello') #调用主函数中p进程传递过来的进程参数 put函数为向队列中添加一条数据。
if __name__ == '__main__':
q = Queue() # 创建一个Queue对象
p = Process(target=f, args=(q,)) #创建一个进程
p.start()
print(q.get()) # 从队列中获取数据
p.join()
from multiprocessing import Queue,Process
def producer(q):
q.put('hello big baby!')
def consumer(q):
print(q.get())
if __name__ == '__main__':
q = Queue()
p = Process(target=producer,args=(q,))
p.start()
p1 = Process(target=consumer,args=(q,))
p1.start()
13)IPC机制(进程间实现通信)
Inter-Process Communication
我们知道进程之间数据是相互隔离的,要想实现进程间的通信(IPC机制),就必须借助于一些技术才可以,比如multiprocessing模块中的:队列和管道,这两种方式都是可以实现进程间数据传输的,由于队列是管道+锁的方式实现,所以我们着重研究队列即可
from multiprocessing import Process
from multiprocessing import JoinableQueue
import time
def task1(q):
x = 100
q.put(x)
print('添加数据')
time.sleep(3)
print(q.get())
def task2(q):
# 想要在task2中获取task1的x
res = q.get()
print(f'获取的数据是{res}')
q.put(9527)
if __name__ == '__main__':
# 产生队列
q = JoinableQueue(10)
# 产生两个不同的子进程
p1 = Process(target=task1, args=(q, ))
p2 = Process(target=task2, args=(q, ))
p1.start()
p2.start()
基于队列实现进程间通信
import time
from multiprocessing import Process, Queue
def f(q):
q.put('hello') #调用主函数中p进程传递过来的进程参数 put函数为向队列中添加一条数据。
if __name__ == '__main__':
q = Queue() # 创建一个Queue对象
p = Process(target=f, args=(q,)) #创建一个进程
p.start()
print(q.get()) # 从队列中获取数据
p.join()
from multiprocessing import Queue,Process
def producer(q):
q.put('hello big baby!')
def consumer(q):
print(q.get())
if __name__ == '__main__':
q = Queue()
p = Process(target=producer,args=(q,))
p.start()
p1 = Process(target=consumer,args=(q,))
p1.start()
14)生产者消费者模型
生产者消费者模型
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据(做包子的)之后不用等待消费者(吃包子的)处理,直接扔给阻塞队列(盘子),消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
基于队列实现生产者消费者模型
# 第一个例子
from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
while True:
res=q.get()
time.sleep(random.randint(1,3))
print('%s 吃 %s' %(os.getpid(),res))
def producer(q):
for i in range(10):
time.sleep(random.randint(1,3))
res='包子%s' %i
q.put(res)
print('%s 生产了 %s' %(os.getpid(),res))
if __name__ == '__main__':
q=Queue()
#生产者们:即厨师们
p1=Process(target=producer,args=(q,))
#消费者们:即吃货们
c1=Process(target=consumer,args=(q,))
#开始
p1.start()
c1.start()
print('主')
# 第二个例子
from multiprocessing import JoinableQueue
from multiprocessing import Process
import time
# 生产者: 生产数据 ---》 队列
def producer(name, food, q):
msg = f'{name} 生产了 {food} 食物'
# 生产一个食物,添加到队列中
q.put(food)
print(msg)
# 消费者: 使用数据 《--- 列队
def customer(name, q):
while True:
try:
time.sleep(0.5)
# 若报错,则跳出循环
food = q.get_nowait()
msg = f'{name} 吃了 {food} 食物!'
print(msg)
except Exception:
break
if __name__ == '__main__':
q = JoinableQueue()
# 创建两个生产者
for line in range(10):
p1 = Process(target=producer, args=('tank1', f'Pig饲料{line}', q))
p1.start()
# 创建两个消费者
c1 = Process(target=customer, args=('jason', q))
c2 = Process(target=customer, args=('sean', q))
c1.start()
c2.start()
4)线程
线程:
1.什么是线程?
进程: 资源单位。
线程: 执行单位。
线程与进程都是虚拟的概念,为了更好表达某种事物。
注意: 开启一个进程,一定会自带一个线程,线程才是真正的执行者。
2.为什么要使用线程?
节省资源的占用。
- 开启进程:
- 1) 会产生一个内存空间,申请一块资源。
- 2) 会自带一个主线程
- 3) 开启子进程的速度要比开启子线程的速度慢
- 开启线程
- 1) 一个进程内可以开启多个线程,从进程的内存空间中申请执行单位。
- 2) 节省资源。
- 开启三个进程:
- 占用三份内存资源
- 开启三个线程:
- 从一个内存资源中,申请三个小的执行单位
- IO密集型用: 多线程
- IO(时间由用户定):
- 阻塞: 切换 + 保存状态
- 计算密集型用: 多进程
- 计算(时间由操作系统定):
- 计算时间很长 ---> 切换 + 保存状态
注意: 进程与进程之间数据是隔离的,线程与线程之间的数据是共享的。
1) 什么是线程
在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程
线程顾名思义,就是一条流水线工作的过程,一条流水线必须属于一个车间,一个车间的工作过程是一个进程
车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一个流水线
流水线的工作需要电源,电源就相当于cpu
所以,进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。
多线程(即多个控制线程):在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。
例如,北京地铁与上海地铁是不同的进程,而北京地铁里的13号线是一个线程,北京地铁所有的线路共享北京地铁所有的资源,比如所有的乘客可以被所有线路拉。
2) 线程的创建开销小
创建进程的开销要远大于线程?
如果我们的软件是一个工厂,该工厂有多条流水线,流水线工作需要电源,电源只有一个即cpu(单核cpu)
一个车间就是一个进程,一个车间至少一条流水线(一个进程至少一个线程)
创建一个进程,就是创建一个车间(申请空间,在该空间内建至少一条流水线)
而建线程,就只是在一个车间内造一条流水线,无需申请空间,所以创建开销小
进程之间是竞争关系,线程之间是协作关系?
车间直接是竞争/抢电源的关系,竞争(不同的进程直接是竞争关系,是不同的程序员写的程序运行的,迅雷抢占其他进程的网速,360把其他进程当做病毒干死)
一个车间的不同流水线式协同工作的关系(同一个进程的线程之间是合作关系,是同一个程序写的程序内开启动,迅雷内的线程是合作关系,不会自己干自己)
3)线程与进程的区别
线程共享创建它的进程的地址空间;进程有自己的地址空间。
线程可以直接访问其进程的数据段;进程有自己的父进程数据段的副本。
线程可以直接与进程中的其他线程通信;进程必须使用进程间通信来与同级进程通信。
新线程很容易创建;新的进程需要父进程的复制。
线程可以对同一进程的线程进行相当大的控制;流程只能对子流程进行控制。
主线程的更改(取消、优先级更改等)可能会影响进程中其他线程的行为;对父进程的更改不会影响子进程。
4)为何要用多线程
多线程指的是,在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。详细的讲分为4点:
- 多线程共享一个进程的地址空间
- 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
- 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。
- 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)
5)多线程的应用举例
开启一个字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程,如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。
6)经典的线程模型(了解)
多个线程共享同一个进程的地址空间中的资源,是对一台计算机上多个进程的模拟,有时也称线程为轻量级的进程
而对一台计算机上多个进程,则共享物理内存、磁盘、打印机等其他物理资源。
多线程的运行也多进程的运行类似,是cpu在多个线程之间的快速切换。
不同的进程之间是充满敌意的,彼此是抢占、竞争cpu的关系,如果迅雷会和QQ抢资源。而同一个进程是由一个程序员的程序创建,所以同一进程内的线程是合作关系,一个线程可以访问另外一个线程的内存地址,大家都是共享的,一个线程干死了另外一个线程的内存,那纯属程序员脑子有问题。
类似于进程,每个线程也有自己的堆栈
不同于进程,线程库无法利用时钟中断强制线程让出CPU,可以调用thread_yield运行线程自动放弃cpu,让另外一个线程运行。
线程通常是有益的,但是带来了不小程序设计难度,线程的问题是:
1. 父进程有多个线程,那么开启的子线程是否需要同样多的线程
如果是,那么附近中某个线程被阻塞,那么copy到子进程后,copy版的线程也要被阻塞吗,想一想nginx的多线程模式接收用户连接。
2. 在同一个进程中,如果一个线程关闭了问题,而另外一个线程正准备往该文件内写内容呢?
如果一个线程注意到没有内存了,并开始分配更多的内存,在工作一半时,发生线程切换,新的线程也发现内存不够用了,又开始分配更多的内存,这样内存就被分配了多次,这些问题都是多线程编程的典型问题,需要仔细思考和设计。
7) POSIX线程(了解)
为了实现可移植的线程程序,IEEE在IEEE标准1003.1c中定义了线程标准,它定义的线程包叫Pthread。大部分UNIX系统都支持该标准,简单介绍如下
8) 在用户空间实现的线程(了解)
线程的实现可以分为两类:用户级线程(User-Level Thread)和内核线线程(Kernel-Level Thread),后者又称为内核支持的线程或轻量级进程。在多线程操作系统中,各个系统的实现方式并不相同,在有的系统中实现了用户级线程,有的系统中实现了内核级线程。
用户级线程内核的切换由用户态程序自己控制内核切换,不需要内核干涉,少了进出内核态的消耗,但不能很好的利用多核Cpu,目前Linux pthread大体是这么做的。
9)在内核空间实现的线程(了解)
内核级线程:切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态;可以很好的利用smp,即利用多核cpu。windows线程就是这样的。
10)用户级与内核级线程的对比(了解)
一: 以下是用户级线程和内核级线程的区别:
- 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。
- 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。
- 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。
- 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。
- 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。
二: 内核线程的优缺点
优点:
- 当有多个处理机时,一个进程的多个线程可以同时执行。
缺点:
- 由内核进行调度。
三: 用户进程的优缺点
优点:
- 线程的调度不需要内核直接参与,控制简单。
- 可以在不支持线程的操作系统中实现。
- 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。
- 允许每个进程定制自己的调度算法,线程管理比较灵活。
- 线程能够利用的表空间和堆栈空间比内核级线程多。
- 同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。
缺点:
- 资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用
11) 混合实现(了解)
用户级与内核级的多路复用,内核同一调度内核线程,每个内核线程对应n个用户线程
12)守护线程
无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁
需要强调的是:运行完毕并非终止运行
1.对主进程来说,运行完毕指的是主进程代码运行完毕
2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
详细解释:
1 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,
2 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' %name)
if __name__ == '__main__':
t=Thread(target=sayhi,args=('egon',))
t.setDaemon(True) #必须在t.start()之前设置
t.start()
print('主线程')
print(t.is_alive())
'''
主线程
True
'''
迷惑人的例子
from threading import Thread
import time
def foo():
print(123)
time.sleep(1)
print("end123")
def bar():
print(456)
time.sleep(3)
print("end456")
t1=Thread(target=foo)
t2=Thread(target=bar)
t1.daemon=True
t1.start()
t2.start()
print("main-------")
13)线程池
from concurrent.futures import ThreadPoolExecutor
import time
# pool只能创建100个线程
pool = ThreadPoolExecutor(100)
def task(line):
print(line)
time.sleep(10)
if __name__ == '__main__':
for line in range(1000):
pool.submit(task, line)
14)其他方法
一 开启线程的两种方式
#方式一
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' %name)
if __name__ == '__main__':
t=Thread(target=sayhi,args=('egon',))
t.start()
print('主线程')
#方式二
from threading import Thread
import time
class Sayhi(Thread):
def __init__(self,name):
super().__init__()
self.name=name
def run(self):
time.sleep(2)
print('%s say hello' % self.name)
if __name__ == '__main__':
t = Sayhi('tony')
t.start()
print('主线程')
343434
二 在一个进程下开启多个线程与在一个进程下开启多个子进程的区别
开启速度
from threading import Thread
from multiprocessing import Process
import os
def work():
print('hello')
if __name__ == '__main__':
#在主进程下开启线程
t=Thread(target=work)
t.start()
print('主线程/主进程')
'''
打印结果:
hello
主线程/主进程
'''
#在主进程下开启子进程
t=Process(target=work)
t.start()
print('主线程/主进程')
'''
打印结果:
主线程/主进程
hello
'''
进程编号
from threading import Thread
from multiprocessing import Process
import os
def work():
print('hello',os.getpid())
if __name__ == '__main__':
#part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
t1=Thread(target=work)
t2=Thread(target=work)
t1.start()
t2.start()
print('主线程/主进程pid',os.getpid())
#part2:开多个进程,每个进程都有不同的pid
p1=Process(target=work)
p2=Process(target=work)
p1.start()
p2.start()
print('主线程/主进程pid',os.getpid())
同一进程内的线程共享数据
from threading import Thread
from multiprocessing import Process
import os
def work():
global n
n=0
if __name__ == '__main__':
# n=100
# p=Process(target=work)
# p.start()
# p.join()
# print('主',n) #毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100
n=1
t=Thread(target=work)
t.start()
t.join()
print('主',n) #查看结果为0,因为同一进程内的线程之间共享进程内的数据
四 练习
练习1
多线程并发的socket服务端
#_*_coding:utf-8_*_
#!/usr/bin/env python
import multiprocessing
import threading
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(('127.0.0.1',8080))
s.listen(5)
def action(conn):
while True:
data=conn.recv(1024)
print(data)
conn.send(data.upper())
if __name__ == '__main__':
while True:
conn,addr=s.accept()
p=threading.Thread(target=action,args=(conn,))
p.start()
客户端
#_*_coding:utf-8_*_
#!/usr/bin/env python
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('127.0.0.1',8080))
while True:
msg=input('>>: ').strip()
if not msg:continue
s.send(msg.encode('utf-8'))
data=s.recv(1024)
print(data)
练习2
三个任务,一个接收用户输入,一个将用户输入的内容格式化成大写,一个将格式化后的结果存入文件
from threading import Thread
msg_l=[]
format_l=[]
def talk():
while True:
msg=input('>>: ').strip()
if not msg:continue
msg_l.append(msg)
def format_msg():
while True:
if msg_l:
res=msg_l.pop()
format_l.append(res.upper())
def save():
while True:
if format_l:
with open('db.txt','a',encoding='utf-8') as f:
res=format_l.pop()
f.write('%s\n' %res)
if __name__ == '__main__':
t1=Thread(target=talk)
t2=Thread(target=format_msg)
t3=Thread(target=save)
t1.start()
t2.start()
t3.start()
五 线程相关的其他方法
Thread实例对象的方法
# isAlive(): 返回线程是否活动的。
# getName(): 返回线程名。
# setName(): 设置线程名。
threading模块提供的一些方法:
# threading.currentThread(): 返回当前的线程变量。
# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
from threading import Thread
import threading
from multiprocessing import Process
import os
def work():
import time
time.sleep(3)
print(threading.current_thread().getName())
if __name__ == '__main__':
#在主进程下开启线程
t=Thread(target=work)
t.start()
print(threading.current_thread().getName())
print(threading.current_thread()) #主线程
print(threading.enumerate()) #连同主线程在内有两个运行的线程
print(threading.active_count())
print('主线程/主进程')
'''
打印结果:
MainThread
<_MainThread(MainThread, started 140735268892672)>
[<_MainThread(MainThread, started 140735268892672)>, <Thread(Thread-1, started 123145307557888)>]
主线程/主进程
Thread-1
'''
主线程等待子线程结束
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' %name)
if __name__ == '__main__':
t=Thread(target=sayhi,args=('egon',))
t.start()
t.join()
print('主线程')
print(t.is_alive())
'''
egon say hello
主线程
False
'''
八 死锁现象与递归锁
进程也有死锁与递归锁,在进程那里忘记说了,放到这里一切说了额
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁
from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()
class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
mutexA.acquire()
print('\033[41m%s 拿到A锁\033[0m' %self.name)
mutexB.acquire()
print('\033[42m%s 拿到B锁\033[0m' %self.name)
mutexB.release()
mutexA.release()
def func2(self):
mutexB.acquire()
print('\033[43m%s 拿到B锁\033[0m' %self.name)
time.sleep(2)
mutexA.acquire()
print('\033[44m%s 拿到A锁\033[0m' %self.name)
mutexA.release()
mutexB.release()
if __name__ == '__main__':
for i in range(10):
t=MyThread()
t.start()
'''
Thread-1 拿到A锁
Thread-1 拿到B锁
Thread-1 拿到B锁
Thread-2 拿到A锁
然后就卡住,死锁了
'''
解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:
mutexA=mutexB=threading.RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止
九 信号量Semaphore
同进程的一样
Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):
from threading import Thread,Semaphore
import threading
import time
# def func():
# if sm.acquire():
# print (threading.currentThread().getName() + ' get semaphore')
# time.sleep(2)
# sm.release()
def func():
sm.acquire()
print('%s get sm' %threading.current_thread().getName())
time.sleep(3)
sm.release()
if __name__ == '__main__':
sm=Semaphore(5)
for i in range(23):
t=Thread(target=func)
t.start()
与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程
十 Event
同进程的一样
线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行
event.isSet():返回event的状态值;
event.wait():如果 event.isSet()==False将阻塞线程;
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False。
55555
例如,有多个工作线程尝试链接MySQL,我们想要在链接前确保MySQL服务正常才让那些工作线程去连接MySQL服务器,如果连接不成功,都会去尝试重新连接。那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作
from threading import Thread,Event
import threading
import time,random
def conn_mysql():
count=1
while not event.is_set():
if count > 3:
raise TimeoutError('链接超时')
print('<%s>第%s次尝试链接' % (threading.current_thread().getName(), count))
event.wait(0.5)
count+=1
print('<%s>链接成功' %threading.current_thread().getName())
def check_mysql():
print('\033[45m[%s]正在检查mysql\033[0m' % threading.current_thread().getName())
time.sleep(random.randint(2,4))
event.set()
if __name__ == '__main__':
event=Event()
conn1=Thread(target=conn_mysql)
conn2=Thread(target=conn_mysql)
check=Thread(target=check_mysql)
conn1.start()
conn2.start()
check.start()
获得cpu数量
import os
print(os.cpu_count())
15)GIL(globl interpreter lock)全局解释锁
'''
定义:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)
'''
结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
注意:GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念, Python完全可以不依赖于GIL
什么是GIL?
GIL本质就是一个互斥锁,相当于执行权限,每个进程内都会存在一把CIL,同一个进程内多个线程必须抢到GIL之后才能使用Cpytho解释器来执行自己的代码,即同一个进程内的多个线程只能使用并发,无法使用并行
为什么要使用GIL?
因为cpython中的垃圾回收机制不是线程安全的,就是为了数据安全才设置的GIL
GIL全局解释器锁的优缺点:
优点: 保证数据的安全
缺点: 单个进程下,开启多个线程,牺牲执行效率,无法实现并行,只能实现并发。
IO密集型下使用多线程.
计算密集型下使用多进程.
IO密集型任务,每个任务4s
- 单核:
- 开启线程比进程节省资源。
- 多核:
- 多线程:
- 开启4个子线程: 16s
- 多进程:
- 开启4个进程: 16s + 申请开启资源消耗的时间
计算密集型任务,每个任务4s
- 单核:
- 开启线程比进程节省资源。
- 多核:
- 多线程:
- 开启4个子线程: 16s
- 多进程:
- 开启多个进程: 4s
16)协程
协程:单线程实现并发
注意:协程是程序员想出来的,操作系统里面只有进程和线程的概念,操作系统调度的是线程
在单线程下实现多个任务遇到IO就切换就可以降低单线程的IO时间,从而最大限度的提升单线程的效率(若不是遇到io,比如遇到计算就切反而会降低效率)
注意:yield并不能解决遇到io就切换的操作
from gevent import monkey # 猴子补丁
monkey.patch_all()
from gevent import spawn
from gevent import joinall
import time
# 注意,gevent模块不能识别他本身以外的IO类型,本身time模块已经封装一份在gevent内,可
# 以调用gevent内部的sleep,如果需要其他类型的IO,则需要打一个猴子补丁
def play(name):
print("%s paly 1 "%name)
time.sleep(2)
print("%s paly 2 "%name)
def eat(name):
print("%s eat 1"%name)
time.sleep(5)
print("%s eat 2"%name)
time_start = time.time()
g1 = spawn(play, 'tank')
g2 = spawn(play, 'sean')
# g1.join()
# g2.join()
joinall([g1, g2]) # 取代join
print('主进程', time.time()-time_start)