工作中需要蛋疼的用python实现并发,在读文档+实践了很久之后,做个总结。注意,本文全是根据我实践中的理解来写的,并且尽量避开了实现细节以及我讨厌使用的asyncio.future和task。因为我觉得你搞并发的时候就应该像golang一样,尽量避免让开发者去搞那些奇怪的功能,而是专注于怎么轻松起一个协程,共享数据并且获得结果。
通过asyncio异步实现并发
asyncio是python原生支持的协程库,性能还可以,这里以3.6版本来讲吧,因为后面几个版本一直在加各种额外feature。
事件循环
python的异步是基于事件循环的。所谓事件循环,可以认为是系统在不断遍历由协程组成的List,然后根据条件来判断是否要运行/挂起当前的协程。当然实际的实现比较复杂,这里不多讨论。
通过事件循环可以实现协程的”假并发”:
可以使用asyncio.get_event_loop()获得当前的事件循环,然后使用run_until_complete方法,运行一个协程并传入参数。一些网上的博主说run_until_complete是阻塞的,但是我亲测这玩意并不一定阻塞,具体需要你自己亲自测试一下。比如我在调用websockets库的serve方法返回的future时这玩意就不会阻塞。我推测是因为内部有一些特殊的实现,在loop没结束时就挂起了,这里不想细究,因为3.7以后就有了asyncio.run,比这个强多了。
当然,一个比较通用的方法是使用loop.create_task运行一个协程。这个方法用起来没什么太多问题,亲测在3.6版本非常好用,跟golang的go一样。
async 和 await
总有人把这俩放在一起说,但是我觉得这俩的意义完全不一样。
async用在函数声明前,标记接下来def的这个函数是一个异步函数,可以放在协程中运行。当然,如果这个函数里不含任何异步的元素,也能用async标记,因为仅仅是个标记嘛。被async标记的函数中可以使用await。
await用于函数调用前,代表以协程的方式运行一个异步函数,并且交出当前协程对线程资源的占用(也就是java的yield)。举个例子,A协程中使用await调用B协程,python解释器会把A协程给暂停掉,然后运行B协程,直到B运行完,或者B遇到了一些事情,交出了资源,才会继续运行A协程。
一旦我们起了一个协程,python就会想尽办法用比较优的方式来调度他。例如,我们用await起了一个IO密集型的函数。当这个函数等待IO时,系统会主动把他暂停掉,继续执行其他的协程。有一个点比较有意思,在我实际应用中,如果A协程用await起了B协程,如果这个B协程没有全部执行完,A协程后续后续的代码并不会被执行。如果你想做到go那样的异步,还是要用事件循环。
asyncio.sleep(0)
亲测,使用sleep(0)可以主动交出当前协程资源,让系统重新调度,而且不会真的sleep。这就可以实现手动切协程了。
通过进程实现真并发
因为Python解释器的原因,如果想要实现真并发,必须要用进程。这里就不讲python的进程池了,原因还是我不喜欢,觉得怪怪的。
Process的启动和停止
起一个process很简单,只要建立一个process对象,然后调用start_processn方法即可。
停止process时需要注意,terminate方法极其不靠谱,很容易关不掉进程。join方法可调用也可不调用,不用担心僵尸进程的问题,因为你新启动一个process的时候系统会自动帮你清理僵尸进程(python文档这么说的,但是我用terminate方法的时候还是经常产生僵尸进程)。事实上,还是os.kill靠谱,因为kill -9是程序一定要接受并响应的信号。
Process之间的信息交换
首先,如果你基于一个类的方法(非静态,非类方法)启了一个进程,那么你需要当心了。虽然python不是使用fork启动的进程(当然你可以手动指定使用fork),它实际产生的效果也是跟fork一样的,那些内存数据都会复制一份,对象的属性值也会是这一时刻的,除了那些专门用于进程通信的对象,如Pipe和Queue以及Value。
关于Pipe和Queue,还是建议去看官方文档,写的很清楚并且易用,亲测数据量大的时候用Queue比较合适。
Daemon Process
Python文档介绍,Daemon Process是指守护进程。守护进程被父进程创建后,不能产生自己的子进程。当父进程停止时,守护进程也会停止。然而!!我自己亲测,父进程停止后,守护进程真的不一定停!!好几次了!所以你还是手动kill一下比较靠谱。
就先写这么多吧,纯靠经验和文档,没有深究源码。
错别字太多