本节内容
一、进程与线程的概念
1.1进程
1.2线程
1.3进程与线程的区别
二、线程
2、1启一个线程
2.2线程的2种调用方式
2.3 join
2.4 守护线程Daemon
2.5线程锁
2.6全局解析锁
2.7递归锁
2.8Semaphore(信号量)
2.9事件Events(红绿灯举例)
2.10queue队列
2.11生产消费者模型
三、进程
3.1多进程
3.2进程间通讯
3.3进程池
一、进程与线程的概念
1.1进程
什么是进程(process)?
以QQ为例, QQ 要以一个整体的形式暴露给操作系统管理,里面包含对各种资源的调用,内存的管理,网络接口的调用等。。。。对各种资源管理的集合 就可以称为: 进程。
程序的执行实例称为进程。每个进程都提供执行程序所需的资源。 进程具有虚拟地址空间,可执行代码,系统对象的打开句柄,安全上下文,唯一进程标识符,环境变量,优先级类,最小和最大工作集大小以及至少一个执行线程。 每个进程都使用单个线程启动,通常称为主线程,但可以从其任何线程创建其他线程。
程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
有了进程为什么还要线程?
进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:
-
进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
-
进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
例如,我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那他如何实现在同一时刻 即能监听键盘输入、又能监听其它人给你发的消息、同时还能把别人发的消息显示在屏幕上呢?你会说,操作系统不是有分时么?但我的亲,分时是指在不同进程间的分时呀, 即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事呀。
再直白一点, 一个操作系统就像是一个工厂,工厂里面有很多个生产车间,不同的车间生产不同的产品,每个车间就相当于一个进程,且你的工厂又穷,供电不足,同一时间只能给一个车间供电,为了能让所有车间都能同时生产,你的工厂的电工只能给不同的车间分时供电,但是轮到你的qq车间时,发现只有一个干活的工人,结果生产效率极低,为了解决这个问题,应该怎么办呢?。。。。没错,你肯定想到了,就是多加几个工人,让几个人工人并行工作,这每个工人,就是线程!
1.2线程
线程: 是操作系统最小的调度单位, 是一串指令的集合。是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。官网链接:https://docs.python.org/3/library/threading.html?highlight=threading#
线程是执行上下文,它是CPU执行指令流所需的所有信息。
假设你正在读一本书,而你现在想休息一下,但是你希望能够从你停下来的确切位置回来并继续阅读。实现这一目标的一种方法是记下页码,行号和字号。因此,阅读书籍的执行环境就是这三个数字。如果你有一个室友,并且她使用相同的技术,她可以在你不使用时拿走这本书,并从她停下的地方继续阅读。然后你可以把它拿回来,并从你原来的地方恢复。
线程以相同的方式工作。 CPU正在给你一种错觉,即它同时进行多次计算。它通过在每次计算上花费一点时间来做到这一点。它可以这样做,因为它具有每个计算的执行上下文。就像您可以与朋友共享一本书一样,许多任务可以共享CPU。在更技术层面上,执行上下文(因此是一个线程)由CPU寄存器的值组成。最后:线程与进程不同。线程是执行的上下文,而进程是与计算相关联的一堆资源。一个进程可以有一个或多个线程。澄清:与进程相关联的资源包括内存页面(进程中的所有线程具有相同的内存视图),文件描述符(例如,打开套接字)和安全凭证(例如,启动该进程的用户的ID)处理)。
1.3进程与线程的区别:
进程 要操作cpu , 必须要先创建一个线程 ,所有在同一个进程里的线程是共享同一块内存空间的。
1、线程共享创建它的进程的地址空间; 进程有自己的地址空间。(线程共享内存空间,进程的内存是独立的)
2、线程可以直接访问其进程的数据段; 进程拥有自己父进程数据段的副本。
3、线程可以直接与其进程的其他线程通信; 进程必须使用进程间通信来与兄弟进程通信。(同一个进程的线程之间可以直接交流,两个进程想通信,必须通过一个中间代理来实现)
4、创建新线程很简单, 创建新进程需要对其父进程进行一次克隆。
5、一个线程可以控制和操作同一进程里的其他线程,但是进程只能操作子进程。
6、对主线程的更改(取消,优先级更改等)可能会影响进程的其他线程的行为; 对父进程的更改不会影响子进程。
二、线程
2.1启动一个线程
举例子,演示一个最简单的多线程
1 import threading 2 import time 3 4 def run(n): 5 print("task",n) 6 time.sleep(2) 7 8 t1 = threading.Thread(target=run,args=("t1",)) 9 t2 = threading.Thread(target=run,args=("t2",)) 10 #总共运行了2秒,因为是并行的2秒 11 t1.start() 12 t2.start() 13 14 #比较,这里会运行4秒,是先后各两秒 15 # run(t1) 16 # run(t2)
2、2线程的2种调用方式
Python threading模块线程有2种调用方式,如下:
1 import threading 2 import time 3 4 5 def sayhi(num): # 定义每个线程要运行的函数 6 7 print("running on number:%s" % num) 8 9 time.sleep(3) 10 11 12 if __name__ == '__main__': 13 t1 = threading.Thread(target=sayhi, args=(1,)) # 生成一个线程实例 14 t2 = threading.Thread(target=sayhi, args=(2,)) # 生成另一个线程实例 15 16 t1.start() # 启动线程 17 t2.start() # 启动另一个线程 18 19 print(t1.getName()) # 获取线程名 20 print(t2.getName())
1 import threading 2 import time 3 4 5 class MyThread(threading.Thread): 6 def __init__(self, num): 7 threading.Thread.__init__(self) 8 self.num = num 9 10 def run(self): # 定义每个线程要运行的函数 11 12 print("running on number:%s" % self.num) 13 14 time.sleep(3) 15 16 17 if __name__ == '__main__': 18 t1 = MyThread(1) 19 t2 = MyThread(2) 20 t1.start() 21 t2.start()
2.3 join
1 import threading 2 import time 3 4 class MyThread(threading.Thread): 5 def __init__(self,n,sleep_time): 6 super(MyThread,self).__init__() 7 self.n = n 8 self.sleep_time = sleep_time 9 def run(self): 10 print("runnint task ",self.n ) 11 time.sleep(self.sleep_time) 12 print("task done,",self.n ) 13 14 15 t1 = MyThread("t1",2) 16 t2 = MyThread("t2",4) 17 18 t1.start() 19 t2.start() 20 21 t1.join() #=wait() 22 t2.join() 23 24 print("main thread....")
1 import threading 2 import time 3 4 def run(n): 5 print("task ",n ) 6 time.sleep(2) 7 print("task done",n) 8 9 start_time = time.time() 10 t_objs = [] #存线程实例 11 for i in range(50): 12 t = threading.Thread(target=run,args=("t-%s" %i ,)) 13 t.start() 14 t_objs.append(t) #为了不阻塞后面线程的启动,不在这里join,先放到一个列表里 15 16 for t in t_objs: #循环线程实例列表,等待所有线程执行完毕 17 t.join() 18 19 20 print("----------all threads has finished...") 21 print("cost:",time.time() - start_time) 22 # run("t1") 23 # run("t2")
在一个进程下开启多个线程与在一个进程下开启多个子进程的区别
1 from threading import Thread 2 from multiprocessing import Process 3 import os 4 5 def work(): 6 print('hello') 7 8 if __name__ == '__main__': 9 #在主进程下开启线程 10 t=Thread(target=work) 11 t.start() 12 print('主线程/主进程') 13 ''' 14 打印结果: 15 hello 16 主线程/主进程 17 ''' 18 19 #在主进程下开启子进程 20 t=Process(target=work) 21 t.start() 22 print('主线程/主进程') 23 ''' 24 打印结果: 25 主线程/主进程 26 hello 27 '''
1 from threading import Thread 2 from multiprocessing import Process 3 import os 4 5 def work(): 6 print('hello',os.getpid()) 7 8 if __name__ == '__main__': 9 #part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样 10 t1=Thread(target=work) 11 t2=Thread(target=work) 12 t1.start() 13 t2.start() 14 print('主线程/主进程pid',os.getpid()) 15 16 #part2:开多个进程,每个进程都有不同的pid 17 p1=Process(target=work) 18 p2=Process(target=work) 19 p1.start() 20 p2.start() 21 print('主线程/主进程pid',os.getpid())
1 from threading import Thread 2 from multiprocessing import Process 3 import os 4 def work(): 5 global n 6 n=0 7 8 if __name__ == '__main__': 9 # n=100 10 # p=Process(target=work) 11 # p.start() 12 # p.join() 13 # print('主',n) #毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100 14 15 16 n=1 17 t=Thread(target=work) 18 t.start() 19 t.join() 20 print('主',n) #查看结果为0,因为同一进程内的线程之间共享进程内的数据
练习
练习一:
1 #_*_coding:utf-8_*_ 2 #!/usr/bin/env python 3 import multiprocessing 4 import threading 5 6 import socket 7 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 8 s.bind(('127.0.0.1',8080)) 9 s.listen(5) 10 11 def action(conn): 12 while True: 13 data=conn.recv(1024) 14 print(data) 15 conn.send(data.upper()) 16 17 if __name__ == '__main__': 18 19 while True: 20 conn,addr=s.accept() 21 22 23 p=threading.Thread(target=action,args=(conn,)) 24 p.start()
1 #_*_coding:utf-8_*_ 2 #!/usr/bin/env python 3 4 5 import socket 6 7 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 8 s.connect(('127.0.0.1',8080)) 9 10 while True: 11 msg=input('>>: ').strip() 12 if not msg:continue 13 14 s.send(msg.encode('utf-8')) 15 data=s.recv(1024) 16 print(data)
练习二:三个任务,一个接收用户输入,一个将用户输入的内容格式化成大写,一个将格式化后的结果存入文件
1 from threading import Thread 2 msg_l=[] 3 format_l=[] 4 def talk(): 5 while True: 6 msg=input('>>: ').strip() 7 if not msg:continue 8 msg_l.append(msg) 9 10 def format_msg(): 11 while True: 12 if msg_l: 13 res=msg_l.pop() 14 format_l.append(res.upper()) 15 16 def save(): 17 while True: 18 if format_l: 19 with open('db.txt','a',encoding='utf-8') as f: 20 res=format_l.pop() 21 f.write('%s\n' %res) 22 23 if __name__ == '__main__': 24 t1=Thread(target=talk) 25 t2=Thread(target=format_msg) 26 t3=Thread(target=save) 27 t1.start() 28 t2.start() 29 t3.start()
线程相关的其他方法
1 Thread实例对象的方法 2 # isAlive(): 返回线程是否活动的。 3 # getName(): 返回线程名。 4 # setName(): 设置线程名。 5 6 threading模块提供的一些方法: 7 # threading.currentThread(): 返回当前的线程变量。 8 # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 9 # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
1 from threading import Thread 2 import threading 3 from multiprocessing import Process 4 import os 5 6 def work(): 7 import time 8 time.sleep(3) 9 print(threading.current_thread().getName()) 10 11 12 if __name__ == '__main__': 13 #在主进程下开启线程 14 t=Thread(target=work) 15 t.start() 16 17 print(threading.current_thread().getName()) 18 print(threading.current_thread()) #主线程 19 print(threading.enumerate()) #连同主线程在内有两个运行的线程 20 print(threading.active_count()) 21 print('主线程/主进程') 22 23 ''' 24 打印结果: 25 MainThread 26 <_MainThread(MainThread, started 140735268892672)> 27 [<_MainThread(MainThread, started 140735268892672)>, <Thread(Thread-1, started 123145307557888)>] 28 主线程/主进程 29 Thread-1 30 '''
主线程等待子线程结束
1 from threading import Thread 2 import time 3 def sayhi(name): 4 time.sleep(2) 5 print('%s say hello' %name) 6 7 if __name__ == '__main__': 8 t=Thread(target=sayhi,args=('egon',)) 9 t.start() 10 t.join() 11 print('主线程') 12 print(t.is_alive()) 13 ''' 14 egon say hello 15 主线程 16 False 17 '''
2.4 守护线程Daemon
有些线程执行后台任务,例如发送keepalive数据包,或执行定期垃圾收集等等。 这些仅在主程序运行时才有用,并且一旦其他非守护程序线程退出就可以将它们终止。
如果没有守护程序线程,您必须跟踪它们,并在程序完全退出之前告诉它们退出。 通过将它们设置为守护程序线程,您可以让它们运行并忘记它们,当程序退出时,任何守护程序线程都会自动终止。
1 import threading 2 import time 3 4 def run(n): 5 print("task ",n ) 6 time.sleep(2) 7 print("task done",n,threading.current_thread()) 8 9 start_time = time.time() 10 t_objs = [] #存线程实例 11 for i in range(50): 12 t = threading.Thread(target=run,args=("t-%s" %i ,)) 13 t.setDaemon(True) #把当前线程设置为守护线程 14 t.start() 15 t_objs.append(t) #为了不阻塞后面线程的启动,不在这里join,先放到一个列表里 16 17 # for t in t_objs: #循环线程实例列表,等待所有线程执行完毕 18 # t.join() 19 20 time.sleep(2) 21 print("----------all threads has finished...",threading.current_thread(),threading.active_count()) 22 print("cost:",time.time() - start_time) 23 # run("t1") 24 # run("t2")
1 import time 2 import threading 3 4 5 def run(n): 6 print('[%s]------running----\n' % n) 7 time.sleep(2) 8 print('--done--') 9 10 11 def main(): 12 for i in range(5): 13 t = threading.Thread(target=run, args=[i, ]) 14 t.start() 15 t.join(1) 16 print('starting thread', t.getName()) 17 18 19 m = threading.Thread(target=main, args=[]) 20 m.setDaemon(True) # 将main线程设置为Daemon线程,它做为程序主线程的守护线程,当主线程退出时,m线程也会退出,由m启动的其它子线程会同时退出,不管是否执行完任务 21 m.start() 22 m.join(timeout=2) 23 print("---main thread done----")
注意:守护程序线程在关闭时突然停止。 他们的资源(例如打开文件,数据库事务等)可能无法正确发布。 如果您希望线程正常停止,请将它们设置为非守护进程并使用合适的信号机制(如Event)。
1 无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁 2 需要强调的是:运行完毕并非终止运行 3 #1.对主进程来说,运行完毕指的是主进程代码运行完毕 4 #2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕 5 6 详细解释: 7 8 #1 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会 9 # 一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束, 10 11 #2 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。 12 # 因为主线程的结束意味着进程的结束,进程整体的资源都将被回收, 13 # 而进程必须保证非守护线程都运行完毕后才能结束。
1 from threading import Thread 2 import time 3 def sayhi(name): 4 time.sleep(2) 5 print('%s say hello' %name) 6 7 if __name__ == '__main__': 8 t=Thread(target=sayhi,args=('egon',)) 9 t.setDaemon(True) #必须在t.start()之前设置 10 t.start() 11 12 print('主线程') 13 print(t.is_alive()) 14 ''' 15 主线程 16 True 17 '''
1 from threading import Thread 2 import time 3 def foo(): 4 print(123) 5 time.sleep(1) 6 print("end123") 7 8 def bar(): 9 print(456) 10 time.sleep(3) 11 print("end456") 12 13 14 t1=Thread(target=foo) 15 t2=Thread(target=bar) 16 17 t1.daemon=True 18 t1.start() 19 t2.start() 20 print("main-------")
2.5线程锁(互斥锁Mutex)
一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况?
1 import time 2 import threading 3 4 5 def addNum(): 6 global num # 在每个线程中都获取这个全局变量 7 print('--get num:', num) 8 time.sleep(1) 9 num -= 1 # 对此公共变量进行-1操作 10 11 12 num = 100 # 设定一个共享变量 13 thread_list = [] 14 for i in range(100): 15 t = threading.Thread(target=addNum) 16 t.start() 17 thread_list.append(t) 18 19 for t in thread_list: # 等待所有线程执行完毕 20 t.join() 21 22 print('final num:', num)
正常来讲,这个num结果应该是0, 但在python 2.7上多运行几次,会发现,最后打印出来的num结果不总是0,为什么每次运行的结果不一样呢? 哈,很简单,假设你有A,B两个线程,此时都 要对num 进行减1操作, 由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=100这个初始变量交给cpu去运算,当A线程去处完的结果是99,但此时B线程运算完的结果也是99,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是99。那怎么办呢? 很简单,每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。
*注:不要在3.x上运行,不知为什么,3.x上的结果总是正确的,可能是自动加了锁
1 import time 2 import threading 3 4 5 def addNum(): 6 global num # 在每个线程中都获取这个全局变量 7 print('--get num:', num) 8 time.sleep(1) 9 lock.acquire() # 修改数据前加锁 10 num -= 1 # 对此公共变量进行-1操作 11 lock.release() # 修改后释放 12 13 14 num = 100 # 设定一个共享变量 15 thread_list = [] 16 lock = threading.Lock() # 生成全局锁 17 for i in range(100): 18 t = threading.Thread(target=addNum) 19 t.start() 20 thread_list.append(t) 21 22 for t in thread_list: # 等待所有线程执行完毕 23 t.join() 24 25 print('final num:', num)
1 import threading 2 import time 3 4 def run(n): 5 lock.acquire() 6 global num 7 num +=1 8 time.sleep(1) 9 lock.release() 10 11 12 lock = threading.Lock() 13 num = 0 14 t_objs = [] #存线程实例 15 for i in range(10): 16 t = threading.Thread(target=run,args=("t-%s" %i ,)) 17 t.start() 18 t_objs.append(t) #为了不阻塞后面线程的启动,不在这里join,先放到一个列表里 19 20 for t in t_objs: #循环线程实例列表,等待所有线程执行完毕 21 t.join() 22 23 print("----------all threads has finished...",threading.current_thread(),threading.active_count()) 24 25 print("num:",num)
2.6全局解析锁 GIL VS Lock
机智的同学可能会问到这个问题,就是既然你之前说过了,Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 注意啦,这里的lock是用户级的lock,跟那个GIL没关系 ,具体我们通过下图来看一下+配合我现场讲给大家,就明白了。
那你又问了, 既然用户程序已经自己有锁了,那为什么C python还需要GIL呢?加入GIL主要的原因是为了降低程序的开发的复杂度,比如现在的你写python不需要关心内存回收的问题,因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序 里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题, 这可以说是Python早期版本的遗留问题。
2.7递归锁 RLOCK
说白了就是在一个大锁中还要再包含子锁
1 __author__ = "Alex Li" 2 3 import threading, time 4 5 6 def run1(): 7 print("grab the first part data") 8 lock.acquire() 9 global num 10 num += 1 11 lock.release() 12 return num 13 14 15 def run2(): 16 print("grab the second part data") 17 lock.acquire() 18 global num2 19 num2 += 1 20 lock.release() 21 return num2 22 23 24 def run3(): 25 lock.acquire() 26 res = run1() 27 print('--------between run1 and run2-----') 28 res2 = run2() 29 lock.release() 30 print(res, res2) 31 32 33 34 35 num, num2 = 0, 0 36 lock = threading.RLock() 37 for i in range(1): 38 t = threading.Thread(target=run3) 39 t.start() 40 41 while threading.active_count() != 1: 42 print(threading.active_count()) 43 else: 44 print('----all threads done---') 45 print(num, num2)
2.8Semaphore(信号量)
互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。
1 __author__ = "Alex Li" 2 3 import threading, time 4 5 6 def run(n): 7 semaphore.acquire() 8 time.sleep(1) 9 print("run the thread: %s\n" % n) 10 semaphore.release() 11 12 13 if __name__ == '__main__': 14 semaphore = threading.BoundedSemaphore(5) # 最多允许5个线程同时运行 15 for i in range(22): 16 t = threading.Thread(target=run, args=(i,)) 17 t.start() 18 while threading.active_count() != 1: 19 pass # print threading.active_count() 20 else: 21 print('----all threads done---') 22
此类表示仅在经过一定时间后才应运行的操作 。与线程一样,通过调用start()方法启动计时器。
通过调用thecancel()方法可以停止计时器(在其动作开始之前)。
计时器在执行其操作之前将等待的时间间隔可能与用户指定的时间间隔不完全相同。
2.9事件Events
事件是一个简单的同步对象;
该事件代表一个内部标志和线程
可以等待设置标志,或者自己设置或清除标志。
event = threading.Event()
#客户端线程可以等待设置标志
event.wait()
#a服务器线程可以设置或重置它
event.set()
event.clear()
如果设置了标志,则wait方法不会执行任何操作。
如果该标志被清除,则等待将被阻塞,直到它再次被设置为止。
任意数量的线程都可以等待同一事件。
1 """ 2 同进程的一样 3 4 线程的一个关键特性是每个线程都是独立运行且状态不可预测。 5 如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。 6 为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志, 7 它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 8 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真, 9 它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行 10 """ 11 12 event.isSet():返回event的状态值; 13 14 event.wait():如果 event.isSet()==False将阻塞线程; 15 16 event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度; 17 18 event.clear():恢复event的状态值为False。
通过Event来实现两个或多个线程间的交互,下面是一个红绿灯的例子,即起动一个线程做交通指挥灯,生成几个线程做车辆,车辆行驶按红灯停,绿灯行的规则。
1 """ 2 例如,有多个工作线程尝试链接MySQL,我们想要在链接前确保MySQL服务正常才让那些工作线程去连接MySQL服务器, 3 如果连接不成功,都会去尝试重新连接。那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作 4 5 6 """ 7 8 from threading import Thread,Event 9 import threading 10 import time,random 11 def conn_mysql(): 12 count=1 13 while not event.is_set(): 14 if count > 3: 15 raise TimeoutError('链接超时') 16 print('<%s>第%s次尝试链接' % (threading.current_thread().getName(), count)) 17 event.wait(0.5) 18 count+=1 19 print('<%s>链接成功' %threading.current_thread().getName()) 20 21 22 def check_mysql(): 23 print('\033[45m[%s]正在检查mysql\033[0m' % threading.current_thread().getName()) 24 time.sleep(random.randint(2,4)) 25 event.set() 26 if __name__ == '__main__': 27 event=Event() 28 conn1=Thread(target=conn_mysql) 29 conn2=Thread(target=conn_mysql) 30 check=Thread(target=check_mysql) 31 32 conn1.start() 33 conn2.start() 34 check.start()
1 __author__ = "Alex Li" 2 3 import time 4 import threading 5 6 7 event = threading.Event() 8 9 def lighter(): 10 count = 0 11 event.set() #先设置绿灯 12 while True: 13 if count >5 and count < 10: #改成红灯 14 event.clear() #把标志位清了 15 print("\033[41;1mred light is on....\033[0m") 16 elif count >10: 17 event.set() #变绿灯 18 count = 0 19 else: 20 print("\033[42;1mgreen light is on....\033[0m") 21 time.sleep(1) 22 count +=1 23 24 def car(name): 25 while True: 26 if event.is_set(): #代表绿灯 27 print("[%s] running..."% name ) 28 time.sleep(1) 29 else: 30 print("[%s] sees red light , waiting...." %name) 31 event.wait() 32 print("\033[34;1m[%s] green light is on, start going...\033[0m" %name) 33 34 35 light = threading.Thread(target=lighter,) 36 light.start() 37 38 car1 = threading.Thread(target=car,args=("Tesla",)) 39 car1.start()
这里还有一个event使用的例子,员工进公司门要刷卡, 我们这里设置一个线程是“门”, 再设置几个线程为“员工”,员工看到门没打开,就刷卡,刷完卡,门开了,员工就可以通过
1 #_*_coding:utf-8_*_ 2 __author__ = 'Alex Li' 3 import threading 4 import time 5 import random 6 7 def door(): 8 door_open_time_counter = 0 9 while True: 10 if door_swiping_event.is_set(): 11 print("\033[32;1mdoor opening....\033[0m") 12 door_open_time_counter +=1 13 14 else: 15 print("\033[31;1mdoor closed...., swipe to open.\033[0m") 16 door_open_time_counter = 0 #清空计时器 17 door_swiping_event.wait() 18 19 20 if door_open_time_counter > 3:#门开了已经3s了,该关了 21 door_swiping_event.clear() 22 23 time.sleep(0.5) 24 25 26 def staff(n): 27 28 print("staff [%s] is comming..." % n ) 29 while True: 30 if door_swiping_event.is_set(): 31 print("\033[34;1mdoor is opened, passing.....\033[0m") 32 break 33 else: 34 print("staff [%s] sees door got closed, swipping the card....." % n) 35 print(door_swiping_event.set()) 36 door_swiping_event.set() 37 print("after set ",door_swiping_event.set()) 38 time.sleep(0.5) 39 door_swiping_event = threading.Event() #设置事件 40 41 42 door_thread = threading.Thread(target=door) 43 door_thread.start() 44 45 46 47 for i in range(5): 48 p = threading.Thread(target=staff,args=(i,)) 49 time.sleep(random.randrange(3)) 50 p.start()
2.10queue队列
当必须在多个线程之间安全地交换信息时,队列在线程编程中特别有用。
class queue.Queue(maxsize = 0)#先入先出
class queue.LifoQueue(maxsize = 0)#last in fisrt out #后进先出
class queue.PriorityQueue(maxsize = 0)#存储数据时可设置优先级的队列 #设置优先级
优先级队列的构造函数。 maxsize是一个整数,用于设置可以放入队列的项目数的上限。达到此大小后,插入将阻止,直到消耗队列项。如果maxsize小于或等于零,则队列大小为无限大。
首先检索最低值的条目(最低值条目是由sorted(list(entries))[0]返回的条目。条目的典型模式是以下形式的元组:(priority_number,data)。
exception queue.Empty 在对空的Queue对象调用非阻塞get()(或get_nowait())时引发异常。异常队列。
- exception
queue.
Full
在已满的Queue对象上调用非阻塞put()(或put_nowait())时引发异常。
Queue.qsize()
Queue.empty() #return如果为空则为真
Queue.full()#如果已满,则返回True
Queue.put(item,block = True,timeout = None)
将项目放入队列。如果可选的args块为true且timeout为None(默认值),则在必要时阻塞,直到有空闲插槽可用。如果timeout是一个正数,则它会阻止最多超时秒,如果在该时间内没有可用的空闲槽,则会引发Full异常。否则(块为假),如果空闲插槽立即可用,则将项目放在队列中,否则引发完全异常(在这种情况下忽略超时)。
Queue.put_nowait(项目) 相当于put(item,False)。
Queue.get(block = True,timeout = None)
从队列中删除并返回一个项目。如果可选的args块为true且timeout为None(默认值),则在必要时阻止,直到某个项可用为止。如果timeout是一个正数,则它会阻止最多超时秒,如果在该时间内没有可用的项,则会引发Empty异常。否则(块为假),如果一个项立即可用,则返回一个项,否则引发Empty异常(在这种情况下忽略超时)。
Queue.get_nowait() 相当于get(False)。
提供了两种方法来支持跟踪守护进程消费者线程是否已完全处理入队任务。
Queue.task_done()
表明以前排队的任务已完成。由队列使用者线程使用。对于用于获取任务的每个get(),对task_done()的后续调用会告知队列任务的处理已完成。
如果join()当前正在阻塞,则它将在所有项目都已处理后恢复(这意味着已为每个已放入队列的项目收到task_done()调用)。
如果调用的次数超过队列中放置的项目,则引发ValueError。
Queue.join()阻止直到队列被消费完毕
看代码实现
1 import queue 2 3 4 q = queue.Queue() 5 6 q.put(1) 7 q.put(2) 8 q.put(3) 9 print(q.get()) 10 print(q.get()) 11 print(q.get())
1 import queue 2 3 4 q = queue.LifoQueue() 5 6 q.put(1) 7 q.put(2) 8 q.put(3) 9 print(q.get()) 10 print(q.get()) 11 print(q.get())
1 import queue 2 3 q = queue.PriorityQueue() 4 5 q.put((-1,"chenronghua")) 6 q.put((3,"hanyang")) 7 q.put((10,"alex")) 8 q.put((6,"wangsen")) 9 10 print(q.get()) 11 print(q.get()) 12 print(q.get()) 13 print(q.get())
2.11生产者消费者模型
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
1 __author__ = "Alex Li" 2 import threading,time 3 4 import queue 5 6 q = queue.Queue(maxsize=10) 7 8 def Producer(name): 9 count = 1 10 while True: 11 q.put("骨头%s" % count) 12 print("生产了骨头",count) 13 count +=1 14 time.sleep(0.1) 15 16 17 18 def Consumer(name): 19 #while q.qsize()>0: 20 while True: 21 print("[%s] 取到[%s] 并且吃了它..." %(name, q.get())) 22 time.sleep(1) 23 24 25 26 p = threading.Thread(target=Producer,args=("Alex",)) 27 c = threading.Thread(target=Consumer,args=("ChengRonghua",)) 28 c1 = threading.Thread(target=Consumer,args=("王森",)) 29 30 31 p.start() 32 c.start() 33 c1.start()
1 import threading 2 import queue 3 4 5 def producer(): 6 for i in range(10): 7 q.put("骨头 %s" % i) 8 9 print("开始等待所有的骨头被取走...") 10 q.join() 11 print("所有的骨头被取完了...") 12 13 14 def consumer(n): 15 while q.qsize() > 0: 16 print("%s 取到" % n, q.get()) 17 q.task_done() # 告知这个任务执行完了 18 19 20 q = queue.Queue() 21 22 p = threading.Thread(target=producer, ) 23 p.start() 24 25 c1 = consumer("李闯")
1 import time,random 2 import queue,threading 3 q = queue.Queue() 4 def Producer(name): 5 count = 0 6 while count <20: 7 time.sleep(random.randrange(3)) 8 q.put(count) 9 print('Producer %s has produced %s baozi..' %(name, count)) 10 count +=1 11 def Consumer(name): 12 count = 0 13 while count <20: 14 time.sleep(random.randrange(4)) 15 if not q.empty(): 16 data = q.get() 17 print(data) 18 print('\033[32;1mConsumer %s has eat %s baozi...\033[0m' %(name, data)) 19 else: 20 print("-----no baozi anymore----") 21 count +=1 22 p1 = threading.Thread(target=Producer, args=('A',)) 23 c1 = threading.Thread(target=Consumer, args=('B',)) 24 p1.start() 25 c1.start()
了解
条件Condition
使得线程等待,只有满足某条件时,才释放n个线程
1 import threading 2 3 def run(n): 4 con.acquire() 5 con.wait() 6 print("run the thread: %s" %n) 7 con.release() 8 9 if __name__ == '__main__': 10 11 con = threading.Condition() 12 for i in range(10): 13 t = threading.Thread(target=run, args=(i,)) 14 t.start() 15 16 while True: 17 inp = input('>>>') 18 if inp == 'q': 19 break 20 con.acquire() 21 con.notify(int(inp)) 22 con.release()
1 def condition_func(): 2 3 ret = False 4 inp = input('>>>') 5 if inp == '1': 6 ret = True 7 8 return ret 9 10 11 def run(n): 12 con.acquire() 13 con.wait_for(condition_func) 14 print("run the thread: %s" %n) 15 con.release() 16 17 if __name__ == '__main__': 18 19 con = threading.Condition() 20 for i in range(10): 21 t = threading.Thread(target=run, args=(i,)) 22 t.start()
定时器
定时器,指定n秒后执行某操作
1 from threading import Timer 2 3 4 def hello(): 5 print("hello, world") 6 7 t = Timer(1, hello) 8 t.start() # after 1 seconds, "hello, world" will be printed
1 from threading import Timer 2 import random,time 3 4 class Code: 5 def __init__(self): 6 self.make_cache() 7 8 def make_cache(self,interval=5): 9 self.cache=self.make_code() 10 print(self.cache) 11 self.t=Timer(interval,self.make_cache) 12 self.t.start() 13 14 def make_code(self,n=4): 15 res='' 16 for i in range(n): 17 s1=str(random.randint(0,9)) 18 s2=chr(random.randint(65,90)) 19 res+=random.choice([s1,s2]) 20 return res 21 22 def check(self): 23 while True: 24 inp=input('>>: ').strip() 25 if inp.upper() == self.cache: 26 print('验证成功',end='\n') 27 self.t.cancel() 28 break 29 30 31 if __name__ == '__main__': 32 obj=Code() 33 obj.check()
三、进程
3.1多进程multiprocessing
multiprocessing是一个使用类似于线程模块的API支持产生进程的包。
多处理包提供本地和远程并发,通过使用子进程而不是线程有效地侧向执行全局解释器锁。
因此,多处理模块允许程序员充分利用给定机器上的多个处理器。 它可以在Unix和Windows上运行
example:先起一个进程,并且里面包含一个字进程
1 #起一个进程,方式与线程类似 2 from multiprocessing import Process 3 import time 4 import threading 5 6 def thread_run(): 7 print(threading.get_ident()) #获取进程ID 8 def func(name): 9 time.sleep(2) 10 print('hello',name) 11 t = threading.Thread(target=thread_run,) 12 t.start() 13 14 15 if __name__=="__main__": 16 for i in range(10): 17 p = Process(target=func,args=('boy_%s'%i,)) 18 p.start() 19 p.join()
example:要显示所涉及的各个进程ID
1 #要显示所涉及的各个进程ID
#每一个子进程都有一个父进程,都是有父进程启动
2 3 import multiprocessing 4 import os 5 6 7 def info(title): 8 print(title) 9 print('module name:',__name__) 10 print('parent process:',os.getppid()) #获取父进程ID 11 print('process id:',os.getpid()) #获取自身进程ID 12 print('\n\n') 13 14 15 def func(name): 16 info('\033[31;1m 启动子进程 func--\033[0m') 17 print('hello',name) 18 19 if __name__=="__main__": 20 info("\033[32;1m 启动父进程\033[0m") 21 p = multiprocessing.Process(target=func,args=('boy',)) 22 p.start() 23 24 """ 25 ############执行结果################ 26 启动父进程 27 module name: __main__ 28 parent process: 6140 #这个进程是pychram的进程 29 process id: 8072 30 31 32 启动子进程 func-- 33 module name: __mp_main__ 34 parent process: 8072 35 process id: 8112 36 37 hello boy 38 39 """
3.2 进程间通讯
两个进程之间的内存是独立的,是不共享的。父进程起了子进程,子进程就独立了;要想实现两个进程间的数据交换必须通过中间件:翻译。
方式一、Queues 实现数据的传递
使用方法跟threading里的queue差不多。(线程queue,只在同一进程下的线程间共享数据,不与其他进程下的线程共享数据;线程queue不能把数据传给其他的进程)
1 #推荐的实现方式
from multiprocessing import Queue,Process
2
3 def func(k):
4 k.put([42,None,'hello'])
5
6
7 if __name__ == '__main__':
8 k = Queue() #进程Q
9 p = Process(target=func,args=(k,)) #克隆了一份k给子进程,不是共享Q
10 p.start()
11 print(k.get())
方式二、Pipes
Pipe()函数返回一个由管道连接的连接对象,默认情况下是双工(双向),实现的是数据的传递。
1 #pipe管道实现方式,类似socket发送和接收消息 2 from multiprocessing import Process,Pipe 3 4 def func(conn): 5 conn.send([42, None, 'hello from child']) #子进程发送消息 6 conn.send([42, None, 'hello from child']) 7 print("", conn.recv()) #子进程接收消息 8 conn.close() 9 10 11 if __name__=='__main__': 12 parent_conn, child_conn = Pipe() 13 p = Process(target=func, args=(child_conn,)) 14 p.start() 15 print("parent", parent_conn.recv()) # 父进程接收信息:prints "[42, None, 'hello']" 16 print("parent", parent_conn.recv()) # prints "[42, None, 'hello']" 17 parent_conn.send("from parent") #父进程发送消息 18 p.join()
1 from multiprocessing import Process, Pipe 2 3 4 def f(conn): 5 conn.send([42, None, 'hello from child']) 6 conn.send([42, None, 'hello from child2']) 7 print("from parent:",conn.recv()) 8 conn.close() 9 10 if __name__ == '__main__': 11 parent_conn, child_conn = Pipe() 12 p = Process(target=f, args=(child_conn,)) 13 p.start() 14 print(parent_conn.recv()) # prints "[42, None, 'hello']" 15 print(parent_conn.recv()) # prints "[42, None, 'hello']" 16 parent_conn.send("pf42280可好") # prints "[42, None, 'hello']" 17 p.join()
方式三、Managers
Manager()返回的管理器对象控制一个服务器进程,该进程保存Python对象并允许其他进程使用代理操作它们。
Manager()返回的管理器将支持类型列表,dict,Namespace,Lock,RLock,Semaphore,BoundedSemaphore,Condition,Event,Barrier,Queue,Value和Array。
例如:
1 #实现共享数据,不再是数据的传递 2 from multiprocessing import Process,Manager 3 import os 4 5 def func(dict1,list1): 6 dict1[os.getpid()] = os.getpid() 7 list1.append(os.getpid()) 8 print(list1) 9 10 if __name__=="__main__": 11 with Manager() as manager: # 类似打开文件的with open() as f 12 dict1 = manager.dict() #生成一个字典,可以在多个进程之间共享和传递 13 list1 = manager.list(range(5)) #生成一个列表,可以在多个进程之间共享和传递,默认有5个值 14 p_list = [] #用于存放进程列表 15 for i in range(10): 16 p = Process(target=func,args=(dict1,list1)) #生成进程 17 p.start() #启动进程 18 p_list.append(p) #把进程添加到p_list列表中 19 for res in p_list: 20 res.join() #等待所有线程结束 21 print(dict1) 22 print(list1) 23 24 25 ''' 26 结果: 27 {6456: 6456, 7528: 7528, 7392: 7392, 3588: 3588, 7744: 7744, 6448: 6448, 4552: 4552, 6776: 6776, 7676: 7676, 7560: 7560} 28 [0, 1, 2, 3, 4, 6456, 7528, 7392, 3588, 7744, 6448, 4552, 6776, 7676, 7560] 29 30 '''
1 from multiprocessing import Process, Manager 2 import os 3 4 def f(d, l): 5 d[1] = '1' 6 d['2'] = 2 7 d["pid%s" %os.getpid()] = os.getpid() 8 l.append(1) 9 print(l,d) 10 11 12 if __name__ == '__main__': 13 with Manager() as manager: 14 d = manager.dict() 15 16 l = manager.list(range(5)) 17 18 p_list = [] 19 for i in range(10): 20 p = Process(target=f, args=(d, l)) 21 p.start() 22 p_list.append(p) 23 for res in p_list: 24 res.join() 25 l.append("from parent") 26 print(d) 27 print(l)
3.3进程池
3.3.1进程同步,进程锁
如果不使用来自不同进程的锁定输出,则可能会混淆不清
1 from multiprocessing import Process,Lock 2 lock = Lock() 3 4 def func(l,i): 5 l.acquire() 6 try: 7 print("hello world",i) 8 finally: 9 l.release() 10 11 12 if __name__=="__main__": 13 for num in range(10): 14 p = Process(target=func,args=(lock,num)) 15 p.start()
3.3.2进程池
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。
进程池中有两个方法:
apply ---同步执行,串行
apply_async ---异步执行,并行
1 """ 2 apply ---同步执行,串行 3 apply_async ---异步执行,并行 4 5 6 注意 7 1、windose起线程池,一定要用if __name__ =="__main__",写在测试代码里 8 2、pool.close 与poo1.join的顺序,先pool.close,再poo1.join 9 3、一定要poo1.join,否则程序直接退出 10 11 """ 12 13 from multiprocessing import Process,Pool,freeze_support 14 import time,os 15 16 def Foo(i): 17 time.sleep(2) 18 print("子进程 in process",os.getpid()) 19 return i+100 20 #回调函数,可以干很多事,比如备份数据,备份完成后会写日志 21 def Bar(arg): 22 print('---->exec done:',arg,os.getpid()) 23 24 if __name__=="__main__": 25 freeze_support() 26 pool = Pool(processes=3) #允许进程池同时放入3个进程 27 print("主进程",os.getpid()) 28 for i in range(10): #进程池起10个进程,但同时只有3个(由pool = Pool(processes=3)限定) 29 pool.apply_async(func=Foo,args=(i,),callback=Bar) #并行 #callback=回调,由主进程完成 30 #pool.apply(func=Foo,args=(i,)) #串行,进入进程池中的3个,一个一个的执行 31 #pool.apply_async(func=Foo,args=(i,))#并行 进入进程池中的3个并行执行 32 print('end') 33 pool.close() #先关闭进程池,再join 34 pool.join() #进程池中进程执行完毕后再关闭,如果注释,主进程完成后,直接关闭
3.3.3apply()和apply_async()的区别
apply():
apply是阻塞的。首先主进程开始运行,碰到子进程,操作系统切换到子进程,等待子进程运行结束后,在切换到另外一个子进程,直到所有子进程运行完毕。然后在切换到主进程,运行剩余的部分。这样跟单进程串行执行没什么区别。
如:
1 import time 2 from multiprocessing import Pool 3 4 def run(count): 5 print('子进程编号:%s'%count) 6 time.sleep(2) 7 print('子进程%s结束'%count) 8 9 if __name__=="__main__": 10 print('开始执行主程序') 11 star_time = time.time() 12 #使用进程池创建子进程 13 pool = Pool(3) 14 print("开始执行子进程") 15 for i in range(10): 16 pool.apply(run,(i,)) 17 pool.close() 18 pool.join() 19 print("主进程结束,总耗时%s" %(time.time() - star_time)) 20 21 22 23 """ 24 开始执行主程序 25 开始执行子进程 26 子进程编号:0 27 子进程0结束 28 子进程编号:1 29 子进程1结束 30 子进程编号:2 31 子进程2结束 32 子进程编号:3 33 子进程3结束 34 子进程编号:4 35 子进程4结束 36 子进程编号:5 37 子进程5结束 38 子进程编号:6 39 子进程6结束 40 子进程编号:7 41 子进程7结束 42 子进程编号:8 43 子进程8结束 44 子进程编号:9 45 子进程9结束 46 主进程结束,总耗时20.345163822174072 47 48 """
可以看到子进程是顺序执行的,且子进程全部执行完毕后才继续执行主进程
apply_async():
apply_async 是异步非阻塞的。即不用等待当前进程执行完毕,随时根据系统调度来进行进程切换。首先主进程开始运行,碰到子进程后,主进程仍可以先运行,等到操作系统进行进程切换的时候,在交给子进程运行。可以做到不等待子进程执行完毕,主进程就已经执行完毕,并退出程序。
如:
1 import time 2 from multiprocessing import Pool 3 4 def run(count): 5 print('子进程编号:%s'%count) 6 time.sleep(2) 7 print('子进程%s结束'%count) 8 9 if __name__=="__main__": 10 print('开始执行主程序') 11 star_time = time.time() 12 #使用进程池创建子进程 13 pool = Pool(3) 14 print("开始执行子进程") 15 for i in range(10): 16 pool.apply_async(run,(i,)) 17 pool.close() 18 pool.join() 19 print("主进程结束,总耗时%s" %(time.time() - star_time)) 20 21 """ 22 进程的切换是操作系统来控制的,是抢占式的切换。 我们首先运行的是主进程,由于主进程代码很简单,主进程一下子就运行完毕了,所以子进程完全没有机会切换到程序就已经结束了。 23 如果我们想要子进程执行完毕后再运行主进程剩余部分,则在恰当位置上加上一句子进程名.join()。这样就告诉主进程等该子进程执行完毕后,再运行主进程剩余部分 24 25 26 开始执行主程序 27 开始执行子进程 28 子进程编号:0 29 子进程编号:1 30 子进程编号:2 31 子进程0结束 32 子进程编号:3 33 子进程1结束 34 子进程编号:4 35 子进程2结束 36 子进程编号:5 37 子进程3结束 38 子进程编号:6 39 子进程4结束 40 子进程编号:7 41 子进程5结束 42 子进程编号:8 43 子进程6结束 44 子进程编号:9 45 子进程7结束 46 子进程8结束 47 子进程9结束 48 主进程结束,总耗时8.29747462272644 49 50 """
可以看到子进程是并行执行的