python协程和子进程混用编程尝试
使用python编程,当程序是IO密集型,很多网友都推荐使用协程代替线程,因为python的多线程因为GIL的原因,并不能使用计算机CPU多核;而协程是微线程,性能更好,资源消耗更少,适合于多并发。
如果程序是计算密集型,则推荐使用多进程编程,因为多进程可以利用到计算机的多核CPU并行计算。
当程序复杂时,必不可少的可能会使用协程和多进程混合编程。
这里做了一个协程和多进程混合编程的尝试,测试代码如下:
1 import asyncio 2 import time 3 import threading 4 from multiprocessing import Process 5 import os 6 7 async def task_a(pid): 8 print("===begin task a.===") 9 for i in range(5): 10 print("in task_a = %s" % pid) 11 await asyncio.sleep(1) 12 print("Resuming task_a = %s" % pid) 13 print("===finish task a.===") 14 return 15 16 17 async def task_b(pid): 18 print("===begin task b.===") 19 p = Process(target=new_loop) 20 p.start() 21 p.join() 22 print("===finish task b.===") 23 24 25 async def task_c(pid): 26 print("===begin task c.===") 27 for i in range(5): 28 print("in task_c = %s" % pid) 29 await asyncio.sleep(2) 30 print("Resuming task_c = %s" % pid) 31 print("===finish task c.===") 32 return 33 34 35 def new_loop(): 36 new_p_loop = asyncio.get_event_loop() 37 print("in new process, pid = {}, ppid={}".format(os.getpid(), os.getppid())) 38 39 tasks = [task_c(os.getpid())] 40 new_p_loop.run_until_complete(asyncio.wait(tasks)) 41 print("finished pid=%s" % (os.getpid())) 42 43 44 if __name__ == '__main__': 45 print("main pid={}, ppid={}".format(os.getpid(), os.getppid())) 46 start = time.perf_counter() 47 48 loop = asyncio.get_event_loop() 49 tasks = [task_a(os.getpid()), task_b(os.getpid())] 50 loop.run_until_complete(asyncio.wait(tasks)) 51 52 end = time.perf_counter() 53 print("cost time = %s" % (end - start))
代码在主进程中使用了协程,创建了两个task:task_a, task_b,这个用于模仿主进程是并发接受消息,然后执行task任务。
task_b中启动了子进程,子进程中又同时启动了协程loop,执行task_c。这个用于模仿task_b是个计算密集型任务,适合启动子进程进行任务执行,从而不影响主进程的运行。
测试代码运行后,打印结果如下:
1 main pid=125948, ppid=128144 2 ===begin task a.=== 3 in task_a = 125948 4 ===begin task b.=== 5 in new process, pid = 121040, ppid=125948 6 ===begin task c.=== 7 in task_c = 121040 8 Resuming task_c = 121040 9 in task_c = 121040 10 Resuming task_c = 121040 11 in task_c = 121040 12 Resuming task_c = 121040 13 in task_c = 121040 14 Resuming task_c = 121040 15 in task_c = 121040 16 Resuming task_c = 121040 17 ===finish task c.=== 18 finished pid=121040 19 ===finish task b.=== 20 Resuming task_a = 125948 21 in task_a = 125948 22 Resuming task_a = 125948 23 in task_a = 125948 24 Resuming task_a = 125948 25 in task_a = 125948 26 Resuming task_a = 125948 27 in task_a = 125948 28 Resuming task_a = 125948 29 ===finish task a.=== 30 cost time = 14.116450799999999
从测试结果来看,程序运行并没有按照我们的预期执行。task_a单独执行需要耗时5秒,task_c单独执行需要耗时10秒,程序执行完耗时14秒多,感觉像串行执行,并没有达到并行执行效果。
从打印日志也可以看出,程序先执行task_a,等待task_a sleep后,切换到执行task_b,task_b启动子进程,然后是先执行完task_c,然后才继续执行task_a,两者没有并行执行。
经过尝试,发现问题出现task_b的p.join()语句上。p.join()的目的是等待子进程执行结束,再退出。然而task_b在执行p.join()语句阻塞时,不会切换到task_a执行,而是将整个主进程的主线程阻塞了。
现将task_b函数代码修改如下:
1 async def task_b(pid): 2 print("===begin task b.===") 3 p = Process(target=new_loop) 4 p.start() 5 while True: 6 if not p.is_alive(): 7 break 8 else: 9 await asyncio.sleep(1) 10 print("===finish task b.===")
程序运行结果如下:
main pid=119276, ppid=128048 ===begin task a.=== in task_a = 119276 ===begin task b.=== in new process, pid = 131008, ppid=119276 ===begin task c.=== in task_c = 131008 Resuming task_a = 119276 in task_a = 119276 Resuming task_a = 119276 in task_a = 119276 Resuming task_c = 131008 in task_c = 131008 Resuming task_a = 119276 in task_a = 119276 Resuming task_a = 119276 in task_a = 119276 Resuming task_c = 131008 in task_c = 131008 Resuming task_a = 119276 ===finish task a.=== Resuming task_c = 131008 in task_c = 131008 Resuming task_c = 131008 in task_c = 131008 Resuming task_c = 131008 ===finish task c.=== finished pid=131008 ===finish task b.=== cost time = 11.0163683
从打印日志来看,代码修改后,task_a和task_c基本是并行执行,耗时为11秒。
总结:
协程task之间并发执行,其实是在同一个线程中切换执行,task切换条件是task有I/O操作或者是asyncio.sleep()操作。
像p.join()操作不会触发task之间的切换操作,会导致协程task阻塞。