前言
首先描述下业务场景,有一个接口,其中存在耗时操作,耗时操作的执行结果将写入数据表中,不需要通过该接口直接返回。
因此理论上该接口可以在耗时操作执行结束前返回。本文中使用多线程后台运行耗时操作,主线程提前返回,实现接口的提前返回。
此外,还尝试使用协程实现,经验证,协程适用于多任务并发处理,遇到耗时操作时自动切换任务,可以缩短多任务总的执行用时,而无法缩短单任务的执行用时,无法实现提前返回,因此不适用该场景。
1 同步任务
如下所示,定义任务,模拟耗时操作。
Python学习交流Q群:906715085### import time def task(): print(\"task start\") print(\"sleep...\") time.sleep(10) print(\"task end\")
如下所示,main 函数中执行任务,return true 用于模拟接口的返回。
如果最后执行 print(“task_result={}”.format®) 表明是同步执行,否则是异步执行。
def main(): print(\"main start\") task() print(\"main end\") return True if __name__ == \'__main__\': r = main() print(\"task_result={}\".format(r))
执行结果如下所示,表明是同步执行。
main start
task start
sleep...
task end
main end
task_result=True
对于接口来说,响应时间是非常重要的性能指标,因此可以通过异步执行实现接口的提前返回,进而降低接口响应时间。
2 异步任务
本文中提到的异步执行指的是后台运行某些代码或功能,不阻塞主程序。
异步执行的常规实现方式是使用多线程/多进程。
2.1 多线程
可以通过多线程的方式实现异步执行,前提是不执行 join() 方法,否则将阻塞主线程。
注意使用多线程实现异步操作时,需要将任务函数作为参数传给线程的构造函数,因此需要修改代码中任务函数的调用方式。
编写并发代码时经常这样重构:把依序执行的for循环体改成函数,以便并发执行。
如下所示,定义 run_task 函数,并将原有的任务函数 task 作为参数传入,其中创建子线程执行任务函数,调用 start() 方法启动线程,不调用 join() 方法主线程继续向下执行。
from threading import Thread def run_task(f, *args, **kwargs): t = Thread(target=f, args=args, kwargs=kwargs) t.start() def main(): print(\"main start\") run_task(task) print(\"main end\") return True if __name__ == \'__main__\': r = main() print(\"task_result={}\".format(r))
执行结果如下所示,表明是异步执行,main 函数在任务函数 task 执行结束前返回。
main start task start main end sleep... task_result=True task end
直接通过多线程实现异步任务的缺点是需要修改任务函数的调用方式,代码的改动较大。
实际上可以通过 多线程+装饰器 的方式将多线程包装一层,在不改变任务函数调用方式的前提下实现异步操作。
2.2 多线程 + 装饰器
定义装饰器,其中创建并启动子线程,用于执行传入的任务函数。本质上与上述多线程的实现相同。
def async_thread(f): def wrapper(*args, **kwargs): t = Thread(target=f, args=args, kwargs=kwargs) t.start() return wrapper
将装饰器置于任务函数的定义处,在不修改原函数定义的前提下,在代码运行期间动态增加功能。
如下所示,通过 async_thread 将同步操作修改为异步操作,同时不修改任务函数的调用方式。
执行函数时,调用 task() 而非 run_task()。
@async_thread def task(): print(\"task start\") print(\"sleep...\") time.sleep(10) print(\"task end\") def main(): print(\"main start\") # run_task(task) task() print(\"main end\") return True
执行结果如下所示,同样是异步执行。
main start task start sleep... main end task_result=True task end
3 异步框架
Python支持多种异步框架,如 Cerely、Tornado、Twisted 等,后续详细介绍。
4 协程
同样,Python支持协程的多种调用方法。本文中使用 asyncio 模块通过协程实现异步操作。
4.1 单任务
如下所示,task 任务与同步代码的区别包括:
•调用 asyncio 模块的 @asyncio.coroutine 装饰器,将生成器声明为协程;
•使用 yield from 语法,等待另外一个协程的返回;
•使用 asyncio.sleep() 代替 time.sleep(),其中 time.sleep() 阻塞,asyncio.sleep() 非阻塞,可以切换任务。
import asyncio @asyncio.coroutine def task(): print(\"task start\") print(\"sleep...\") yield from asyncio.sleep(10) print(\"task end\")
main 方法与同步代码的区别包括:
•调用 asyncio.get_event_loop() 创建事件循环,事件循环用于运行异步任务和回调;
•调用 loop.run_until_complete() 将协程对象交给事件循环,并阻塞直到协程运行结束才返回;
•调用 loop.close(),关闭事件循环。
def main(): print(\"main start\") loop = asyncio.get_event_loop() c = task() loop.run_until_complete(c) loop.close() print(\"main end\") return True
执行结果如下所示,表明是同步执行。
main starttask startsleep…task endmain endtask_result=True
协程同步执行的原因是 asyncio 是一个基于事件循环的异步IO模块,其中通过 yield from 将协程的 async.sleep() 的控制权交给事件循环,然后挂起协程,也就是说 yield from 等待协程 async.sleep() 的返回结果。等待过程中让出CPU执行权,由事件循环决定何时唤醒 async.sleep(),接着向后执行代码。
可见,协程的作用体现在切换,切换生效的前提是存在多任务,当前代码中只有一个任务,因此体现不出协程的作用。
4.2 多任务
如下所示创建多任务,并将协程对象交给事件循环。
def main(): print(\"main start\") loop = asyncio.get_event_loop() # c = task() # loop.run_until_complete(c) c1 = task() c2 = task() loop.run_until_complete(asyncio.wait([c1, c2])) loop.close() print(\"main end\") return True
执行结果如下所示,第二个任务的 task start 在第一个任务的 task end 之前执行,表明任务已切换。
main start
task start
sleep...
task start
sleep...
task end
task end
main end
task_result=True
同时,与协程执行单任务相同,task_result=True 也是最后执行,可见协程并不适用于函数的提前返回。
因此,可以根据场景选择使用多线程/多进程与协程。
•多线程/多进程,适用于后台执行,函数提前返回的场景;
•协程,适用于多任务并发执行,可以降低IO等待,最终降低多任务总的执行用时,无法降低单任务的执行用时。
当然,多线程/多进程也可以用于并发执行,需要根据具体场景选择使用哪种方式实现。
5 结论
本文中的业务场景是实现接口的提前返回,后台运行耗时操作。
本文中提到的异步执行指的是后台运行某些代码或功能,不阻塞主程序。
异步执行的常规实现方式是使用多线程/多进程,缺点是需要将原函数作为参数传递给线程/进程,因此需要修改调用的函数名。
结合装饰器可以实现在不改变任务函数调用方式的前提下实现异步操作。
此外,使用协程也可以实现异步执行。但是协程适用于多任务并发处理,遇到耗时操作时自动切换任务,可以缩短多任务总的执行用时,而无法缩短单任务的执行用时,无法实现提前返回,因此不适用该场景。
事实上,Python中由于GIL的存在,在一个进程中每次只能有一个线程在运行,因此多线程处理并发的实现方式也是任务线程的切换。某个线程想要运行,首先要获得GIL锁,然后遇到IO或者超时的时候释放GIL锁,给其余的线程去竞争,竞争成功的线程获得GIL锁得到下一次运行的机会。
因此,Python中多线程适用于IO密集型应用。
多进程处理并发的实现方式与CPU的核数有关。对于单核CPU,一个时间点只能运行一个进程,因此只能并发,无法并行,多进程通过时间片轮转的方式轮流占用CPU。对于多核CPU,一个时间点每个CPU可以运行一个进程,因此可以实现并行。
因此,Python中多进程适用于CPU密集型应用。
多线程/多线程与协程的相同点是都可以实现任务的切换,不同点是切换机制的实现方式。
一个程序想要同时处理多个任务,必须提供一种能够记录任务执行进度的机制。多线程/多线程由CPU提供该机制,协程由事件循环提供。
6 小技巧
装饰器+多线程 可用于比较优雅地实现异步任务,不阻塞主程序。
协程的优势在于多任务并发处理,可以实现轻量级的任务切换。
因此,使用过程中需要先确认业务场景,如果目标是提起返回,可以使用多线程,如果目标是多任务并发处理,可以使用协程。
今天分享的就那么多,到这里就没有了,喜欢的点赞收藏,不懂的评论留言,一般我看见都会回复的。下一篇见啦。
来源:https://www.cnblogs.com/123456feng/p/16082506.html
本站部分图文来源于网络,如有侵权请联系删除。