第五十七节,线程、进程、协程
线程
首先弄清进程和线程之间的区别,这一点是非常重要的。线程与进程的不同之处在于,它们共享状态、内存和资源。对于线程来说,这个简单的区别既是它的优势,又是它的缺点。一方面,线程是轻量级的,并且相互之间易于通信,但另一方面,它们也带来了包括死锁、争用条件和高复杂性在内的各种问题。幸运的是,由于 GIL 和队列模块,与采用其他的语言相比,采用 Python 语言在线程实现的复杂性上要低得多。无论是创建进程或者线程都是为了实现并发操作
Python进程、线程之间的原理图
计算机有进程和线程的目的:提高执行效率
计算机默认有主进程和主线程
进程:
优点:同时利用多个CPU,能够同时进行多个操作
缺点:耗费资源(重新开辟内存空间)
进程不是越多越好,理论上CPU个数(核数)=进程个数
计算密集型适用于进程,因为计算之类的需要CPU运算(占用CPU)
线程:
优点:共享内存,IO操作时,创造并发操作
缺点:枪战资源
线程不是越多越好,具体案例具体分析,请求上下文切换耗时
IO密集型适用于线程,IO操作打开文件网络通讯类,不需要占用CPU,只是由CPU调度一下(不占用CPU)
自定义进程和线程:注意python解释器自带了主进程和主线程,比如在代码文件里没有定义线程和进程,程序也能运行就是靠的解释器自带主进程的主线程执行的
自定义进程:
由主进程创建,子进程
自定义线程:
由主线程创建,子线程
GIL全局解释器锁:
GIL全局解释器锁在进程入口,控制着进程数量与CPU的相应
threading线程模块
线程是应用程序中工作的最小单元
threading 模块建立在 _thread 模块之上。thread 模块以低级、原始的方式来处理和控制线程,而 threading 模块通过对 thread 进行二次封装,提供了更方便的 api 来处理线程。
Thread()创建线程对象【有参】
使用方法:赋值变量 = 模块名称.Thread(target=事件函数,args=元祖类型事件函数的实际参数) 如函数多个参数,元祖里就是多个元素
格式:t = threading.Thread(target=show, args=(i,))
currentThread()获取当前线程【无参】
使用方法:自定义变量 = threading模块名称.currentThread()
格式:current_thread = threading.currentThread()
start()激活线程【无参】
使用方法:thread对象变量.start()
格式:t.start()
#!/usr/bin/env python # -*- coding:utf8 -*- import threading #导入线程模块 import time #导入时间模块 def show(arg): #定义函数 time.sleep(3) #睡眠3秒 print('线程'+str(arg)) #打印线程加循环次数 for i in range(10): #定义一个10次循环 t = threading.Thread(target=show, args=(i,)) #用threading模块的Thread类来创建子线程对象 t.start() #激活子线程 print("默认主线程等待子线程完成任务后,主线程停止") # 输出 # 默认主线程等待子线程完成任务后,主线程停止 # 线程0 # 线程5 # 线程8 # 线程3 # 线程6 # 线程4 # 线程1 # 线程7 # 线程2 # 线程9
上述代码创建了10个“前台”线程,然后控制器就交给了CPU,CPU根据指定算法进行调度,分片执行指令。
自定义线程类
import threading import time class MyThread(threading.Thread): def __init__(self,num): threading.Thread.__init__(self) self.num = num def run(self):#定义每个线程要运行的函数 print("running on number:%s" %self.num) time.sleep(3) if __name__ == '__main__': t1 = MyThread(1) t2 = MyThread(2) t1.start() t2.start()
getName()获取线程的名称【无参】
使用方法:thread对象变量.getName()
格式:t.getName()
#!/usr/bin/env python # -*- coding:utf8 -*- import threading #导入线程模块 import time #导入时间模块 def show(arg): #定义函数 time.sleep(3) #睡眠3秒 print('线程'+str(arg)) #打印线程加循环次数 for i in range(10): #定义一个10次循环 t = threading.Thread(target=show, args=(i,)) #用threading模块的Thread类来创建子线程对象 t.start() #激活子线程 print(t.getName()) #获取线程的名称 print("默认主线程等待子线程完成任务后,主线程停止") # 输出 # Thread-1 # Thread-2 # Thread-3 # Thread-4 # Thread-5 # Thread-6 # Thread-7 # Thread-8 # Thread-9 # Thread-10 # 默认主线程等待子线程完成任务后,主线程停止 # 线程2 # 线程1 # 线程0 # 线程9 # 线程8 # 线程6 # 线程3 # 线程5 # 线程7 # 线程4
setName()设置线程的名称【有参】
使用方法:thread对象变量.setName("要设置的线程名称")
格式:t.setName("jhf")
name获取或设置线程的名称【无参】
使用方法:thread对象变量.name
格式:t.name
#!/usr/bin/env python # -*- coding:utf8 -*- import threading #导入线程模块 import time #导入时间模块 def show(arg): #定义函数 time.sleep(3) #睡眠3秒 print('线程'+str(arg)) #打印线程加循环次数 for i in range(10): #定义一个10次循环 t = threading.Thread(target=show, args=(i,)) #用threading模块的Thread类来创建子线程对象 t.setName("jhf") #设置线程的名称 print(t.name) #获取或设置线程的名称 t.start() #激活子线程 print("默认主线程等待子线程完成任务后,主线程停止") # 输出 # jhf # jhf # jhf # jhf # jhf # jhf # jhf # jhf # jhf # jhf # 默认主线程等待子线程完成任务后,主线程停止 # 线程1 # 线程0 # 线程2 # 线程7 # 线程5 # 线程3 # 线程4 # 线程9 # 线程8 # 线程6
is_alive()判断线程是否为激活状态返回布尔值【无参】
使用方法:thread对象变量.is_alive()
格式:t.is_alive()
#!/usr/bin/env python # -*- coding:utf8 -*- import threading #导入线程模块 import time #导入时间模块 def show(arg): #定义函数 time.sleep(3) #睡眠3秒 print('线程'+str(arg)) #打印线程加循环次数 for i in range(10): #定义一个10次循环 t = threading.Thread(target=show, args=(i,)) #用threading模块的Thread类来创建子线程对象 t.start() #激活子线程 a = t.is_alive() #判断线程是否为激活状态返回布尔值 print(a) #打印出返回值 print("默认主线程等待子线程完成任务后,主线程停止") # 输出 # True # 默认主线程等待子线程完成任务后,主线程停止 # 线程3 # 线程4 # 线程2 # 线程5 # 线程1 # 线程0 # 线程6 # 线程7 # 线程8 # 线程9
isAlive()判断线程是否为激活状态返回布尔值【无参】
使用方法:thread对象变量.isAlive()
格式:t.isAlive()
#!/usr/bin/env python # -*- coding:utf8 -*- import threading #导入线程模块 import time #导入时间模块 def show(arg): #定义函数 time.sleep(3) #睡眠3秒 print('线程'+str(arg)) #打印线程加循环次数 for i in range(10): #定义一个10次循环 t = threading.Thread(target=show, args=(i,)) #用threading模块的Thread类来创建子线程对象 t.start() #激活子线程 a = t.isAlive() #判断线程是否为激活状态返回布尔值 print(a) #打印出返回值 print("默认主线程等待子线程完成任务后,主线程停止") # 输出 # True # 默认主线程等待子线程完成任务后,主线程停止 # 线程3 # 线程4 # 线程2 # 线程5 # 线程1 # 线程0 # 线程6 # 线程7 # 线程8 # 线程9
setDaemon() 设置为后台线程或前台线程,也就是定义主线程是否等待子线程执行完毕后,主线程才停止【有参】
(默认:False);通过一个布尔值设置线程是否为守护线程,必须在执行start()方法之后才可以使用。如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止;如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止
使用方法:thread对象变量.setDaemon(布尔值)
格式:t.setDaemon(True)
#!/usr/bin/env python # -*- coding:utf8 -*- import threading #导入线程模块 import time #导入时间模块 def show(arg): #定义函数 time.sleep(3) #睡眠3秒 print('线程'+str(arg)) #打印线程加循环次数 for i in range(10): #定义一个10次循环 t = threading.Thread(target=show, args=(i,)) #用threading模块的Thread类来创建子线程对象 t.setDaemon(True) #设置为后台线程或前台线程,也就是定义主线程是否等待子线程执行完毕后,主线程才停止 t.start() #激活子线程 # 输出 # 主线没等子线程执行完,主线程就停止了,所以没输出信息
isDaemon()判断是否为守护线程,也就是主线程是否等待子线程执行完成后,才停止主线程,返回布尔值【无参】
使用方法:thread对象变量.isDaemon()
格式:t.isDaemon()
#!/usr/bin/env python # -*- coding:utf8 -*- import threading #导入线程模块 import time #导入时间模块 def show(arg): #定义函数 time.sleep(3) #睡眠3秒 print('线程'+str(arg)) #打印线程加循环次数 for i in range(10): #定义一个10次循环 t = threading.Thread(target=show, args=(i,)) #用threading模块的Thread类来创建子线程对象 t.start() #激活子线程 a = t.isDaemon() #判断是否为守护线程,也就是主线程是否等待子线程执行完成后,才停止主线程返回布尔值 print(a) #打印布尔值 # 输出 # False # 线程1 # 线程3 # 线程0 # 线程5 # 线程2 # 线程4 # 线程9 # 线程6 # 线程8 # 线程7
ident获取线程的标识符。线程标识符是一个非零整数,只有在调用了start()方法之后该属性才有效,否则它只返回None。【无参】
使用方法:thread对象变量.ident
格式:t.ident
#!/usr/bin/env python # -*- coding:utf8 -*- import threading #导入线程模块 import time #导入时间模块 def show(arg): #定义函数 time.sleep(3) #睡眠3秒 print('线程'+str(arg)) #打印线程加循环次数 for i in range(10): #定义一个10次循环 t = threading.Thread(target=show, args=(i,)) #用threading模块的Thread类来创建子线程对象 t.start() #激活子线程 a = t.ident #获取线程的标识符。线程标识符是一个非零整数,只有在调用了start()方法之后该属性才有效,否则它只返回None。 print(a) #打印线程的标识符 # 输出 # 10040 # 13172 # 12096 # 4456 # 10200 # 844 # 2200 # 2440 # 2968 # 12756 # 线程3 # 线程2 # 线程1 # 线程0 # 线程7 # 线程9 # 线程8 # 线程4 # 线程5 # 线程6
join()逐个执行每个线程,等待一个线程执行完毕后继续往下执行,该方法使得多线程变得无意义【有参可选】
有参可选,参数为等待时间,秒为单位,如t.join(1) 就是一个线程不在是等待它执行完,而是只等待它1秒后继续下一个线程
使用方法:thread对象变量.join()
格式:t.join()
#!/usr/bin/env python # -*- coding:utf8 -*- import threading #导入线程模块 import time #导入时间模块 def show(arg): #定义函数 time.sleep(1) #睡眠3秒 print('线程'+str(arg)) #打印线程加循环次数 for i in range(10): #定义一个10次循环 t = threading.Thread(target=show, args=(i,)) #用threading模块的Thread类来创建子线程对象 t.start() #激活子线程 t.join() #逐个执行每个线程,执行完毕后继续往下执行,该方法使得多线程变得无意义 # 输出 # 线程0 # 线程1 # 线程2 # 线程3 # 线程4 # 线程5 # 线程6 # 线程7 # 线程8 # 线程9
run()线程被cpu调度后自动执行线程对象的run方法
使用方法:thread对象变量.run()
格式:t.run()
线程锁threading.RLock和threading.Lock
我们使用线程对数据进行操作的时候,如果多个线程同时修改某个数据,可能会出现不可预料的结果,为了保证数据的准确性,引入了锁的概念。
没有线程锁的情况列如:一个全局变量值为50,创建10条线程让每条线程累计减一,输出的结果是10个40,原因是10条线程同时减一就减去了10,所以打印出来就是10个40了
未使用锁
#!/usr/bin/env python # -*- coding:utf8 -*- import threading #导入线程模块 import time #导入时间模块 globals_num = 50 #设置一个变量 def Func(a): #定义一个函数 global globals_num #将变量转换成全局变量,函数里可以调用 globals_num -= 1 #全局变量减1 time.sleep(1) #睡眠1秒 print(globals_num,a) #打印全局变量减少后的结果,和函数传进来的值 for i in range(10): #创建一个10次循环 t = threading.Thread(target=Func,args=(i,)) #创建线程对象 t.start() #激活线程 # 输出 没有线程锁,线程之间抢占了数据资源 # 40 5 # 40 3 # 40 6 # 40 4 # 40 0 # 40 2 # 40 1 # 40 9 # 40 8 # 40 7
根据上列情况可以看出,没有线程锁,线程之间抢占了数据资源
线程锁就是将线程锁定,一个线程执行完毕后释放锁后在执行第二个线程
RLock()定义线程锁对象
使用方法:定义对象变量 = threading模块名称.RLock()
格式:lock = threading.RLock()
acquire()获得锁,将线程锁定,一个线程执行完毕释放锁后在执行第二个线程
使用方法:线程锁对象变量.acquire()
格式:lock.acquire()
release()释放线程锁
使用方法:线程锁对象变量.release()
格式:lock.release()
使用锁
#!/usr/bin/env python # -*- coding:utf8 -*- import threading #导入线程模块 import time #导入时间模块 globals_num = 50 #设置一个变量 lock = threading.RLock() def Func(a): #定义一个函数 lock.acquire() # 获得锁,将线程锁定,一个线程执行完毕后在执行第二个线程 global globals_num #将变量转换成全局变量,函数里可以调用 globals_num -= 1 #全局变量减1 time.sleep(1) #睡眠1秒 print(globals_num,a) #打印全局变量减少后的结果,和函数传进来的值 lock.release() # 释放锁 for i in range(10): #创建一个10次循环 t = threading.Thread(target=Func,args=(i,)) #创建线程对象 t.start() #激活线程 # 输出 将线程锁定,一个线程执行完毕后在执行第二个线程 # 49 0 # 48 1 # 47 2 # 46 3 # 45 4 # 44 5 # 43 6 # 42 7 # 41 8 # 40 9
threading.RLock和threading.Lock 的区别
RLock允许在同一线程中被多次acquire。而Lock却不允许这种情况。 如果使用RLock,那么acquire和release必须成对出现,即调用了n次acquire,必须调用n次的release才能真正释放所占用的琐。
import threading lock = threading.Lock() #Lock对象 lock.acquire() lock.acquire() #产生了死琐。 lock.release() lock.release()
import threading rLock = threading.RLock() #RLock对象 rLock.acquire() rLock.acquire() #在同一线程内,程序不会堵塞。 rLock.release() rLock.release()
threading.Event事件对象
Event()创建标识事件对象,全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞
python线程的事件用于主线程控制其他线程的执行,事件主要提供了三个方法 set、wait、clear。
事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞。
当线程执行的时候,如果flag为False,则线程会阻塞,当flag为True的时候,线程不会阻塞。它提供了本地和远程的并发性。
Event事件对象的方法有
wait([timeout]) : 堵塞线程,直到Event对象内部标识位被设为True或超时(如果提供了参数timeout)。
set() :将标识位设为Ture
clear() : 将标识位设为False。
isSet() :判断标识位是否为Ture。
#!/usr/bin/env python # -*- coding:utf8 -*- import threading def do(event): print('start') event.wait() #堵塞线程,直到Event对象内部标识位被设为True或超时(如果提供了参数timeout) print('execute') event_obj = threading.Event() #创建标识事件对象 for i in range(10): t = threading.Thread(target=do, args=(event_obj,)) #创建线程对象 t.start() #激活线程 event_obj.clear() #将标识设置为False inp = input('input:') if inp == 'true': event_obj.set() #将标识设置为True # 输出 # start # start # start # start # start # start # start # start # start # input:true # execute # execute # execute # execute # execute # execute # execute # execute # execute # execute
threading.BoundedSemaphore信号对象
是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。
BoundedSemaphore()创建信号对象【有参】
使用方法:定义变量.threading.BoundedSemaphore(最大允许线程数)
格式:semaphore = threading.BoundedSemaphore(5)
BoundedSemaphore信号对象的方法有
acquire()获取信号
release()释放信号
#!/usr/bin/env python # -*- coding:utf8 -*- import threading,time #导入线程模块,和时间模块 semaphore = threading.BoundedSemaphore(5) #最多允许5个线程同时运行 def run(n): #创建函数 semaphore.acquire() #获取信号 time.sleep(1) print("run the thread: %s" %n) #semaphore.release() #释放信号 for i in range(20): t = threading.Thread(target=run,args=(i,)) t.start() # 输出 # run the thread: 3 # run the thread: 2 # run the thread: 0 # run the thread: 1 # run the thread: 4
threading.Condition条件对象
使得线程等待,只有满足某条件时,才释放n个线程
Condition()创建条件对象【无参】
使用方法:定义变量.threading.Condition()
格式:con = threading.Condition()
Condition条件对象的方法有
acquire()
wait()
release()
notify()
#!/usr/bin/env python # -*- coding:utf8 -*- import threading con = threading.Condition() def run(n): con.acquire() con.wait() print("run the thread: %s" %n) con.release() for i in range(10): t = threading.Thread(target=run, args=(i,)) t.start() while True: inp = input('>>>') if inp == 'q': break con.acquire() con.notify(int(inp)) con.release()
queue模块
Queue 就是对队列,它是线程安全的
举例来说,我们去肯德基吃饭。厨房是给我们做饭的地方,前台负责把厨房做好的饭卖给顾客,顾客则去前台排队领取做好的饭。这里的前台就相当于我们的队列。
这个模型也叫生产者-消费者模型。
Queue()创建队列对象【有参】
使用方法:定义变量 = queue.Queue(对列长度数) 0表示长度无限制
格式:message = queue.Queue(10)
Queue对象方法有:
join()等到队列为空的时候,在执行别的操作【无参】
qsize()返回队列的大小(不可靠),因为获取后有可能有新数据加入【无参】
empty()清空队列里的所有数据
full()检查队列是否为满,当队列满的时候,返回True,否则返回False(不可靠),因为获取后有可能有新数据加入【无参】
put(放入对列的数据必选, block=True, timeout=None) 向队列里放入数据(生产)【有参】
将数据放入对列尾部(生产),数据必须存在,可以参数block默认为True,表示当队列满时,会等待队列给出可用位置,为False时为非阻塞,此时如果队列已满,会引发queue.Full 异常。
可选参数timeout,表示 会阻塞设置的时间,过后,如果队列无法给出放入item的位置,则引发 queue.Full 异常
get(block=True, timeout=None)移除并返回队列头部的一个值(消费)【有参】
可选参数block默认为True,表示获取值的时候,如果队列为空,则阻塞,为False时,不阻塞,若此时队列为空,则引发 queue.Empty异常。
可选参数timeout,表示会阻塞设置的时候,过后,如果队列为空,则引发Empty异常。
put_nowait(放入对列的数据必选)向队列里放入数据(生产)【有参】,如果队列满时不阻塞,不等待队列给出可用位置,引发 queue.Full 异常
get_nowait()移除并返回队列头部的一个值(消费)【无参】,如果队列空时不阻塞,引发 queue.Full 异常
对列模型-生产者-消费者
#!/usr/bin/env python # -*- coding:utf8 -*- import queue #导入列队模块 import threading #导入线程模块 message = queue.Queue(10) #定义列队对象,设置列队长度 def producer(i): #定义生产者函数 while True: message.put("生产") #向队列里放数据 def consumer(i): #定义消费者函数 while True: msg = message.get() #从队列里取数据 print(msg) #打印出从队列里取出 for i in range(12): #创建12条线程生产,也就是有12条线程向队列里放数据 t = threading.Thread(target=producer, args=(i,)) #创建线程对象 t.start() #激活线程 for i in range(10): #创建10条线程消费,也就是有10条线程从列队里取数据 t = threading.Thread(target=consumer, args=(i,)) #创建线程对象 t.start() #激活线程 # 输出 # 生产 # 生产 # 生产 # 生产 # 生产 # 生产 # 生产 # 生产 # 生产 # 生产 # 生产 # 生产 # 生产
对列模型-生产者-消费者原理图
multiprocessing进程模块
multiprocessing是python的多进程管理包,和threading.Thread类似。直接从侧面用subprocesses替换线程使用GIL的方式,由于这一点,multiprocessing模块可以让程序员在给定的机器上充分的利用CPU。
在multiprocessing中,通过创建Process对象生成进程,然后调用它的start()方法,
Process()创建进程对象【有参】
注意:wds系统下必须if __name__ == "__main__"才能创建进程,我们调试没关系,以后在Linux系统没这个问题
使用方法:定义变量 = multiprocessing.Process(target=要创建进程的函数, args=元祖类型要创建进程函数的参数、多个参数逗号隔开)
格式:t = multiprocessing.Process(target=f1, args=(133,))
start()激活进程【无参】
使用方法:Process对象变量.start()
格式:t.start()
创建10条进程
#!/usr/bin/env python # -*- coding:utf8 -*- import multiprocessing #导入进程模块 def f1(r): #创建函数 print(r) #打印传值 if __name__ == "__main__": #wds系统下必须if __name__ == "__main__"才能创建进程,我们调试没关系,以后在Linux系统没这个问题 for i in range(10): #循环10次,创建10条进程 t = multiprocessing.Process(target=f1, args=(133,)) #创建进程对象 t.start() #激活进程 # 输出 # 133 # 133 # 133 # 133 # 133 # 133 # 133 # 133 # 133 # 133
daemon主进程是否等待子进程执行完毕后,在停止主进程,daemon=True(主进程不等待子进程)、daemon=False(主进程等待子进程)
使用方法:Process对象变量.daemon=True或者False
格式:t.daemon = False
#!/usr/bin/env python # -*- coding:utf8 -*- import multiprocessing #导入进程模块 def f1(r): #创建函数 print(r) #打印传值 if __name__ == "__main__": #wds系统下必须if __name__ == "__main__"才能创建进程,我们调试没关系,以后在Linux系统没这个问题 for i in range(10): #循环10次,创建10条子进程 t = multiprocessing.Process(target=f1, args=(133,)) #创建进程对象 t.daemon = True #主进程是否等待子进程执行完毕后,在停止主进程 t.start() #激活进程 # 输出 #daemon = False 主进程没等子进程执行完,主进程就停止了,所以没有打印出信息
join()逐个执行每个进程,等待一个进程执行完毕后继续往下执行,该方法使得进程程变得无意义【有参可选】
有参可选,参数为等待时间,秒为单位,如t.join(1) 就是一个进程不在是等待它执行完,而是只等待它1秒后继续下一个进程
#!/usr/bin/env python # -*- coding:utf8 -*- import multiprocessing #导入进程模块 import time def f1(r): #创建函数 time.sleep(1) print(r) #打印传值 if __name__ == "__main__": #wds系统下必须if __name__ == "__main__"才能创建进程,我们调试没关系,以后在Linux系统没这个问题 for i in range(10): #循环10次,创建10条子进程 t = multiprocessing.Process(target=f1, args=(133,)) #创建进程对象 t.start() #激活进程 t.join() #逐个执行每个进程,等待一个进程执行完毕后继续往下执行 # 输出 # 133 # 133 # 133 # 133
进程各自持有一份数据,默认无法共享数据
所以相当于每一个进程有一份自己的数据,每个进程操作数据时,操作的属于自己的一份数据
#!/usr/bin/env python # -*- coding:utf8 -*- import multiprocessing #导入进程模块 li = [] #创建空列表 def f1(i): #创建函数 li.append(i) #追加列表 print("列表",li) #打印追加后的列表 if __name__ == "__main__": #wds系统下必须if __name__ == "__main__"才能创建进程,我们调试没关系,以后在Linux系统没这个问题 for i in range(10): #循环10次,创建10条子进程,进程各自持有一份数据,默认无法共享数据,所以相当于每一个进程有一个f1函数,每个进程在追加列表时追加的属于自己的一份f1函数 t = multiprocessing.Process(target=f1, args=(i,)) #创建进程对象 t.start() #激活进程 # 输出 # 列表 [0] # 列表 [2] # 列表 [5] # 列表 [3] # 列表 [1] # 列表 [6] # 列表 [8] # 列表 [7] # 列表 [4] # 列表 [9]
进程原理图
注意:由于进程之间的数据需要各自持有一份,所以创建进程需要的非常大的开销。
进程数据共享
注意:进程与进程之间无法共享数据,要想共享数据就得用特殊方法,在主进程创建特殊数据,然后几个子进程来共享这个主进程的特殊数据
方法一
Array()创建数组,数组,定义数组必须要定义数组的长度,数组里必须是统一的数据类型【有参】
使用方法:Array('指定数组数据类型',列表样式的数组元素)
指定数组数据类型有:
'c': ctypes.c_char, 'u': ctypes.c_wchar, 'b': ctypes.c_byte, 'B': ctypes.c_ubyte, 'h': ctypes.c_short, 'H': ctypes.c_ushort, 'i': ctypes.c_int, 'I': ctypes.c_uint, 'l': ctypes.c_long, 'L': ctypes.c_ulong, 'f': ctypes.c_float, 'd': ctypes.c_double
利用Array()数组来多进程共享数据(不推荐使用)
#!/usr/bin/env python # -*- coding:utf8 -*- import multiprocessing #导入进程模块 temp = multiprocessing.Array('i', [11,22,33,44,]) #创建数组 def Foo(i): #定义函数 #第一条进程,将100加0等于100,重新赋值给数组里的第0个元素,也就是将数组里的11改成了100 #第二条进程,将100加1等于101,重新赋值给数组里的第1个元素,也就是将数组里的22改成了101 temp[i] = 100+i for item in temp: #循环数组 print(i,'----->',item) #循环打印进程线,和数组元素 print("\n") if __name__ == "__main__": #wds系统下必须if __name__ == "__main__"才能创建进程,我们调试没关系,以后在Linux系统没这个问题 for i in range(2): p = multiprocessing.Process(target=Foo,args=(i,)) #创建进程对象 p.start() #激活进程 # 输出 # 0 -----> 100 # 0 -----> 22 # 0 -----> 33 # 0 -----> 44 # 1 -----> 11 # 1 -----> 101 # 1 -----> 33 # 1 -----> 44
方法二
Manager()创建特殊字典对象【无参】
使用方法:定义变量 = multiprocessing模块名称.Manager()
格式:manage = multiprocessing.Manager()
dict()创建特殊字典【可选参数】
参数为字段值,一般都不设置,为空即可,注意:这个特殊字典和前面的字典有所区别,但大部分使用方法相同,可以索引,可以values()取值
使用方法:殊字典对象变量.dict()
格式:dic = manage.dict()
利用特殊字典dict()来多进程共享数据【推荐】
#!/usr/bin/env python # -*- coding:utf8 -*- import multiprocessing #导入进程模块 def Foo(i,dic): #定义函数 dic[i] = 100+i #100加以进程线,索引方式重新赋值给特殊字典 print(dic.values()) #打印特殊字典的所有值 if __name__ == '__main__': #wds系统下必须if __name__ == "__main__"才能创建进程,我们调试没关系,以后在Linux系统没这个问题 manage = multiprocessing.Manager() #创建特殊字典对象 dic = manage.dict() #创建特殊字典,值为空 for i in range(10): #循环创建10条进程 p = multiprocessing.Process(target=Foo,args=(i,dic,)) #创建进程对象 p.start() #激活进程 p.join() #等待一个进程执行完,在执行第二个进程,否则主进程停止了无法共享数据,因为共享数据时在主进程里 # 输出 # [100] # [100, 101] # [100, 101, 102] # [100, 101, 102, 103] # [100, 101, 102, 103, 104] # [100, 101, 102, 103, 104, 105] # [100, 101, 102, 103, 104, 105, 106] # [100, 101, 102, 103, 104, 105, 106, 107] # [100, 101, 102, 103, 104, 105, 106, 107, 108] # [100, 101, 102, 103, 104, 105, 106, 107, 108, 109]
进程-队列-生产者-消费者【不推荐】严重耗费内存资源
#!/usr/bin/env python # -*- coding:utf8 -*- import multiprocessing #导入进程模块 def f2(i,q): #定义生产者函数 q.put("h1") #向队列里放数据 def f(i,q): #定义消费者函数 print(i,q.get()) #向列队里取数据 if __name__ == '__main__': #wds系统下必须if __name__ == "__main__"才能创建进程,我们调试没关系,以后在Linux系统没这个问题 q = multiprocessing.Queue() #定义对列 for i in range(10): #创建10条进程生产 p = multiprocessing.Process(target=f2, args=(i,q,)) #创建进程对象 p.start() #激活进程 for i in range(10): #创建10条进程消费 p = multiprocessing.Process(target=f, args=(i,q,)) #创建进程对象 p.start() #激活进程 # 输出 # 1 h1 # 0 h1 # 8 h1 # 5 h1 # 3 h1 # 6 h1 # 7 h1 # 2 h1 # 9 h1 # 4 h1
进程锁
#!/usr/bin/env python # -*- coding:utf8 -*- import multiprocessing #导入进程模块 def Foo(lock,temp,i): #创建函数 """ 将第0个数加100 """ lock.acquire() #获取进程锁 temp[0] = 100+i #100加上进程线循环次数,重新赋值给进程循环次数对应下标数组里的值 for item in temp: #循环数组 print(i,'----->',item) #打印出进程循环次数,和数组 lock.release() #释放进程锁 print("\n") if __name__ == '__main__': #wds系统下必须if __name__ == "__main__"才能创建进程,我们调试没关系,以后在Linux系统没这个问题 lock = multiprocessing.RLock() #创建进程锁对象 temp = multiprocessing.Array('i', [11, 22, 33, 44]) #创建数组 for i in range(5): #循环创建5条子进程 p = multiprocessing.Process(target=Foo,args=(lock,temp,i,)) #创建进程对象 p.start() #激活进程 # 输出 # 0 -----> 100 # 0 -----> 22 # 0 -----> 33 # 0 -----> 44 # 1 -----> 101 # 1 -----> 22 # 1 -----> 33 # 1 -----> 44 # 3 -----> 103 # 3 -----> 22 # 3 -----> 33 # 3 -----> 44 # 2 -----> 102 # 2 -----> 22 # 2 -----> 33 # 2 -----> 44 # 4 -----> 104 # 4 -----> 22 # 4 -----> 33 # 4 -----> 44
进程池
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。进程池Python有提供
Pool()创建进程池对象【有参】
默认进程池里没有进程,只有在向进程池申请进程的时候,进程池才创建进程
使用方法:定义变量 = multiprocessing模块名称.Pool(定义进程数)
格式:pool = multiprocessing.Pool(5)
close()进程池里的进程执行完毕后关闭进程池连接【无参】
使用方法:进程池对象变量.close()
格式:pool.close()
terminate()不等进程池里的进程执行完毕,立即关闭进程池连接
使用方法:进程池对象变量.terminate()
格式:pool.terminate()
join()主进程等待进程池里的子进程全部执行完成后,主进程才停止【可选参数】
可选参数,不写就是等待直到子进程全部执行完成后,主进程才停止,写了就是只等待指定的时间,时间到了就停止主进程,不管子进程有没有完成
使用方法:进程池对象变量.join(可选参数秒)
格式:pool.join()
向进程池申请进程的方法
apply()向进程池申请一条进程,进程函数执行完后将进程放回进程池,【有参】
注意:apply()向进程池申请的进程不是并发的,是一个进程执行完毕后在执行一个进程,以此循环的,
apply()向进程池申请进程的时候,进程池创建的每一个进程都有一个,进程对象.join()方法,所以进程才是排队执行的,这里我们需要知道一下
使用方法:进程池对象变量.apply(func=要执行的进程函数名称,args=(执行函数的实际参数、多个参数逗号隔开))
格式:pool.apply(func=Foo,args=(i,))
#!/usr/bin/env python # -*- coding:utf8 -*- import multiprocessing #导入进程模块 import time #导入时间模块 def Foo(i): #定义进程执行函数 time.sleep(1) print(i+100) if __name__ == '__main__': #wds系统下必须if __name__ == "__main__"才能创建进程,我们调试没关系,以后在Linux系统没这个问题 pool = multiprocessing.Pool(5) #定义进程池对象 for i in range(10): #循环向进程池申请10条进程 pool.apply(func=Foo,args=(i,)) #向进程池申请进程 # 输出 # 100 # 101 # 102 # 103 # 104 # 105 # 106 # 107 # 108 # 109
apply_async()向进程池申请一条进程,进程函数执行完后将进程放回进程池,并且可以设置进程执行函数的回调函数
注意:apply_async()向进程池申请的进程是并发的,也就是申请了几条进程就是同时执行几条进程的,回调函数的形式参数、接收的进程执行函数的返回值
apply_async()向进程池申请进程的时候,进程池创建的进程都没有,进程对象.join()方法,所以进程都是并发的,而且进程对象的daemon=True,也就是主进程不会等待子进程执行完毕就终止,所以使用apply_async()向进程池申请进程的时候,进程申请后,要使用close()进程池里的进程执行完毕后关闭进程池连接,join()主进程等待进程池里的子进程全部执行完成后,主进程才停止,否则会报错
使用方法:进程池对象变量.apply_async(func=要执行的进程函数名称,args=(执行函数的实际参数、多个参数逗号隔开),callback=回调函数名称)
格式:pool.apply_async(func=Foo,args=(i,),callback=f2)
#!/usr/bin/env python # -*- coding:utf8 -*- import multiprocessing #导入进程模块 import time #导入时间模块 def Foo(i): #定义进程执行函数 time.sleep(1) print(i+100) return "返回值,返回给回调函数的,形式参数" def f2(a): #执行函数的回调函数,形式参数等于执行函数的返回值 print(a) #打印进程执行函数返回的值 if __name__ == '__main__': #wds系统下必须if __name__ == "__main__"才能创建进程,我们调试没关系,以后在Linux系统没这个问题 pool = multiprocessing.Pool(5) #定义进程池对象 for i in range(10): #循环向进程池申请10条进程 pool.apply_async(func=Foo,args=(i,),callback=f2) #向进程池申请进程,并设置执行函数,和回调函数 pool.close() #进程池里的进程执行完毕后关闭进程池连接 pool.join()#主进程等待进程池里的子进程全部执行完成后,主进程才停止 # 输出 # 100 # 返回值,返回给回调函数的,形式参数 # 101 # 返回值,返回给回调函数的,形式参数 # 102 # 返回值,返回给回调函数的,形式参数 # 103 # 返回值,返回给回调函数的,形式参数 # 104 # 返回值,返回给回调函数的,形式参数 # 105 # 返回值,返回给回调函数的,形式参数 # 106 # 返回值,返回给回调函数的,形式参数 # 107 # 返回值,返回给回调函数的,形式参数 # 108 # 返回值,返回给回调函数的,形式参数 # 109 # 返回值,返回给回调函数的,形式参数
apply_async()向进程池申请进程原理图
自定义线程池
自定义线程池版本一
这个版本并不理想,但是简单
#!/usr/bin/env python # -*- coding:utf8 -*- import queue #导入队列模块 import threading #导入线程模块 """定义一个类""" class ThreadPool(object): #创建类 def __init__(self, max_num=20): #初始化 self.queue = queue.Queue(max_num) #定义普通字段等于,长度为20的队列 for i in range(max_num): #设置20次循环 self.queue.put(threading.Thread) #循环向队列里,放入20个线程对象名称 def get_thread(self): #定义get_thread方法 return self.queue.get() #返回在队列里取出线程名称 def add_thread(self): #定义add_thread方法 self.queue.put(threading.Thread) #向队列里放入一个线程对象名称 """创建一个类对象""" pool = ThreadPool(20) #创建类对象,初始化__init__方法 """定义线程执行函数""" def func(arg, p): #定义线程执行函数 print(arg) #打印线程数,也就是第几次循环线程 import time #导入时间模块 time.sleep(2) #睡眠2秒 p.add_thread() #向队列放入一个线程对象名称,创建一个线程对象的时候,就从队列里拿走一个线程对象名称,所有要在放回一个回去 """创建线程""" for i in range(30): #定义一个30次循环 thread = pool.get_thread() #在列队里拿出一个线程名称 t = thread(target=func, args=(i, pool)) #在队列里拿出一个线程对象名称,创建一个线程对象,传入线程执行函数和参数 t.start() #激活线程 # 输出 # 0 # 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
自定义线程池版本一原理图
自定义线程池版本二【推荐使用】
ThreadPool源码模块,使用方法将ThreadPool源码模块文件,放到工程目录下,导入模块使用
#!/usr/bin/env python # -*- coding:utf8 -*- """线程池源码""" import queue #导入队列模块 import threading #导入线程模块 import contextlib #导入上下文管理模块 StopEvent = object() #设置全局变量,停止标志 class ThreadPool(object): #创建类 """ ThreadPool()创建线程池类对象,有参:线程最大数量【使用方法:定义线程池对象变量 = ThreadPool(线程最大数量)】 run()向线程池申请一条线程,有参【使用方法:线程对象.run(线程执行函数,(执行函数参数),回调函数)】 close()让所有线程执行完毕后,停止线程,无参【使用方法:线程对象.close()】 terminate()无论是否还有任务,终止线程,有参:是否立即清空队列里的数据,默认yes清空,no不清空【使用方法:线程对象.terminate(yes或no)】 """ def __init__(self, max_num, max_task_num = None): """ 初始化ThreadPool类数据,创建队列,记录线程最大数量,创建、记录真实创建的线程列表 创建、记录空闲线程列表 """ if max_task_num: #判断max_task_num如果有值 self.q = queue.Queue(max_task_num) #创建队列,队列长度为max_task_num的值 else: #如果max_task_num没有值 self.q = queue.Queue() #创建队列,队列的长度没有限制 self.max_num = max_num #创建普通字段max_num等于,定义ThreadPool类对象的第一个实际参数,也就是最多能创建的线程数 #self.cancel = False #创建普通字段cancel = False self.terminal = False #创建普通字段terminal = False,以这个标识决定线程是否继续到队列取任务 self.generate_list = [] #创建generate_list空列表,记录真实创建的线程 self.free_list = [] #创建free_list空列表,记录空闲线程 def run(self, func, args, callback=None): w = (func, args, callback,) #将传进来的,线程执行函数名称和执行函数参数,以及回调函数名称,组合成一个元组赋值给w变量 self.q.put(w) #将元祖放入对列中 """判断空闲线程列表等于0,也就是空闲列表里没有空闲的线程时, 并且真实创建的线程列表里的线程数小于总线程数,执行generate_thread方法 """ if len(self.free_list) == 0 and len(self.generate_list) < self.max_num: self.generate_thread() #执行generate_thread方法 def generate_thread(self): t = threading.Thread(target=self.call) #创建一个线程,线程执行函数为call方法 t.start() #激活线程 def call(self): """ 循环去获取任务函数并执行任务函数 """ current_thread = threading.currentThread() #获取当前线程 self.generate_list.append(current_thread) #将获取到的当前线程,追加到真实创建的线程列表里 event = self.q.get() #到队列里取出,run方法放入队列的元组 while event != StopEvent: #如果到队列里取到的不等于停止标志,说明是元组,如果是元组开始循环 """将元组里的3个元素,分别赋值给3个变量,第一个是线程执行函数名称,第二个是线程执行函数参数,第三个是回调函数名称""" func, arguments, callback = event #success = True #自定义一个执行函数是否执行成功标识,默认True表示成功 try: result = func(*arguments) #执行线程执行函数,并接收执行函数参数 except Exception as e: #如果执行错误 result = e #如果线程执行函数出错,线程执行函数返回值等于错误信息 if callback is not None: #如果回调函数存在 try: callback(result) #执行回调函数,并将执行函数返回结果传值给回调函数的形式参数result except Exception as e: pass """标记线程空闲了""" if self.terminal: #判断terminal变量是True event = StopEvent #如果是True就想列队里放入线程停止标志 else: with self.worker_state(self.free_list, current_thread): #执行里面代码块前先执行上下文管理函数 event = self.q.get() #到队列里取出,run方法放入队列的元组,没有就等待 else: self.generate_list.remove(current_thread) #如果在队列里取出的不是元组,而是停止标识,就在真实创建的线程列表里移除当前的线程 def close(self): """ 执行完所有的任务后,所有线程停止 """ full_size = len(self.generate_list) #获取真实创建的线程列表里的线程个数 while full_size: #循环,真实创建线程列表里,的线程个数对应的次数 self.q.put(StopEvent) #每循环一次,向队列里加一个全局变量StopEvent,停止标识 full_size -= 1 #每循环一次让循环次数减一 def terminate(self, qkdl = "yes"): """ 无论是否还有任务,终止线程 """ if qkdl == "yes": self.terminal = True #将是否继续到队列取任务的判断变量修改为True,向队列里放停止标识,使其线程停止 self.q.empty() #清空队列里的所有数据 zuiduo = len(self.generate_list) #检查真实创建线程列表里有多少个线程 while zuiduo: #循环真实创建线程列表里线程数,对应次数 self.q.put(StopEvent) #每循环一次向队列里放停止标识 zuiduo -= 1 #每循环一次,减少一次循环次数 else: self.terminal = True #将是否继续到队列取任务的判断变量修改为True,向队列里放停止标识,使其线程停止 zuiduo = len(self.generate_list) #检查真实创建线程列表里有多少个线程 while zuiduo: #循环真实创建线程列表里线程数,对应次数 self.q.put(StopEvent) #每循环一次向队列里放停止标识 zuiduo -= 1 #每循环一次,减少一次循环次数 @contextlib.contextmanager #定义上下文管理装饰器 def worker_state(self, state_list, worker_thread): #定义上下文管理装饰器函数 """ 用于记录线程中正在等待的线程数 """ state_list.append(worker_thread) #执行代码块前执行,将当前线程追加到空闲线程列表 try: yield #遇到yield,跳出装饰器函数,执行代码块后,在回到yield这里向下执行 finally: state_list.remove(worker_thread) #执行代码块后执行,将当前线程移除空闲线程列表
ThreadPool自定义线程池版本二模块使用说明
首先from xxx import ThreadPool 导入模块
ThreadPool()创建线程池对象【有参】
使用方法:定义线程池对象变量 = ThreadPool模块名称.ThreadPool(线程池线程最大数量)
格式:pool = ThreadPool.ThreadPool(5)
run()到线程池申请一条线程【有参】
使用方法:线程池对象.run(线程执行函数,(线程执行函数的参数),回调函数)
格式:pool.run(f1,(i,),f2)
close()执行完所有的任务后,所有线程停止【无参】
使用方法:线程池对象.close()
格式:pool.close()
terminate()无论是否还有任务,终止线程【可选参数】
使用方法:线程池对象.terminate()
参数默认为yes终止线程前清空队列,no为终止线程不清空队列
格式:pool.terminate()
ThreadPool自定义线程池版本二使用代码
#!/usr/bin/env python # -*- coding:utf8 -*- from lib.ska import ThreadPool #导入线程池模块 import time #导入时间模块 def f2(i): #定义回调函数 print(i) #打印线程执行函数的返回值,回调函数的形式参数接收的,线程执行函数的返回值 def f1(i): #定义线程执行函数 time.sleep(1) #睡眠1秒 print(i) #打印申请线程时传进来的参数 return "回调" #返回值给回调函数 pool = ThreadPool.ThreadPool(5) #创建线程池对象 for i in range(100): #循环 pool.run(f1,(i,),f2) #到线程池申请线程 pool.close() #执行完所有的任务后,所有线程停止 #pool.terminate() #无论是否还有任务,终止线程
自定义线程池版本二原理图
协程
协程又叫(微线程),就是在一个线程里可以创建多个协程,由程序员来控制什么时候执行那条协程,协程可以用一条线程,同时执行多个任务,适用于IO密集型场景
协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续)。协程,则只使用一个线程,在一个线程中规定某个代码块执行顺序。
协程的适用场景:当程序中存在大量不需要CPU的操作时(IO),适用于协程;
greenlet最基础协程模块 第三方模块
greenlet()创建协程对象【有参】
使用方法:自定义变量 = greenlet(协程执行函数)
格式:gr1 = greenlet(test1)
switch()执行指定的协程,switch()前面为指定要执行的协程对象名称【无参】
如果协程执行函数里,遇到switch()时就会跳出当前协程执行函数,并记录当前跳出位置,去执行遇到switch()指定的线程,记录的跳出位置下次进入时,从跳出位置开始
使用方法:要执行的协程对象变量.switch()
格式:gr1.switch()
简单协程代码
#!/usr/bin/env python # -*- coding:utf8 -*- from greenlet import greenlet #导入协程模块 def test1(): #定义协程执行函数 print(12) #打印12 gr2.switch() #执行指定的协程,switch()前面为指定要执行的协程对象名称,执行gr2协程 print(34) #打印34 gr2.switch() #执行指定的协程,switch()前面为指定要执行的协程对象名称,执行gr1协程 def test2(): #定义协程执行函数 print(56) #打印56 gr1.switch() #执行指定的协程,switch()前面为指定要执行的协程对象名称,执行gr1协程 print(78) #打印78 gr1 = greenlet(test1) #创建协程对象,传入协程执行函数 gr2 = greenlet(test2) #创建协程对象,传入协程执行函数 gr1.switch() #执行指定的协程,switch()前面为指定要执行的协程对象名称,执行gr1协程
简单协程原理图
gevent协程模块
gevent协程模块是基于greenlet模块改进的,也是第三方模块
joinall()创建协程对象【有参】
参数是列表类型的,创建协程spawn()方法
使用方法:模块名称.joinall([gevent.spawn(线程执行函数)])
格式:gevent.joinall([gevent.spawn(foo), gevent.spawn(bar), gevent.spawn(ba),])
spawn()创建协程【有参】
参数是协程执行函数名称
使用方法:gevent模块名称.joinall([gevent.spawn(协程执行函数名称), gevent.spawn(协程执行函数名称), gevent.spawn(协程执行函数名称),])
格式:gevent.joinall([gevent.spawn(foo), gevent.spawn(bar), gevent.spawn(ba),])
sleep()跳出协程执行函数,执行协程对象里的,下一个协程,并记录当前跳出位置,再次进入当前协程执行函数时,从当前跳出位置开始
使用方法:模块名称.sleep(0)
格式:gevent.sleep(0)
gevent简单协程代码
#!/usr/bin/env python # -*- coding:utf8 -*- import gevent #导入协程模块 def foo(): #定义协程执行函数 print(12) gevent.sleep(0) #执行协程对象里下一条协程,如果已经是最后一条协程,就返回第一条协程执行 print(34) def bar(): #定义协程执行函数 print(56) gevent.sleep(0) #执行协程对象里下一条协程,如果已经是最后一条协程,就返回第一条协程执行 print(78) def ba(): #定义协程执行函数 print(910) gevent.sleep(0) #执行协程对象里下一条协程,如果已经是最后一条协程,就返回第一条协程执行 print(1112) gevent.joinall([ #定义协程对象 gevent.spawn(foo), #创建协程 gevent.spawn(bar), #创建协程 gevent.spawn(ba), #创建协程 ]) # 输出 # 12 # 56 # 910 # 34 # 78 # 1112
gevent简单协程原理图
遇到IO操作自动切换:
#!/usr/bin/env python # -*- coding:utf8 -*- from gevent import monkey; monkey.patch_all() #导入模块目录里的,gevent目录,里的monkey模块的patch_all()方法 import gevent #导入协程模块 import requests #模拟浏览器请求模块 def f(url): #创建协程执行函数 print('GET: %s' % url) #打印字符串格式化GET:+函数参数url resp = requests.get(url) #将url发送http请求 data = resp.text #获取http字符串代码 print('%d 请求返回 %s.' % (len(data), url)) #打印字符串格式化,http字符串代码字符串数和url地址 """ 相当于三条协程同时发url请求,那条协程先完成请求就先获取那条协程的返回数据 也就是,协程在发IO请求时不会等待发送的请求返回数据完成,就自动切换下一条线程开始发下一条请求了,所有协程请求发完后,那条请求先返回数据完成,就先获取那条请求的数据 """ gevent.joinall([ #创建协程对象 gevent.spawn(f, 'https://www.python.org/'), #创建协程,传入执行函数和执行函数参数 gevent.spawn(f, 'https://www.yahoo.com/'), #创建协程,传入执行函数和执行函数参数 gevent.spawn(f, 'https://github.com/'), #创建协程,传入执行函数和执行函数参数 ])
遇到IO操作自动切换原理图