[asyncio随记一]asyncio的实现原理和关键源码分析

    最近在python3.7上用asyncio做项目,实现web的服务端,一边从GitHub和StackOverflow上抄代码,一边在看asyncio相关的源码,所学所思,姑且写在这里。

为什么会出现协程(coroutine)这种设计?

    多线程(thread)也是同时执行多个任务的一种设计,为什么有了多线程,我们还要设计协程,它有什么不同呢?

    首先,多线程的目的是什么?

    一种说法,利用多核的性能,让代码占用尽可能的计算资源,运行快一点。这算是个原因吧,我们简称为并行(parallelism)。在这里,我还不想抠字眼讨论并发(cocurrent)和并行。

    另一种说法,我们程序有些任务是cpu密集型(逻辑计算比较多),有些任务是IO密集型(读写文件或网络比较多),如果遇到IO密集型计算,比如从网站上下载一个大文件,这时候如果是阻塞下载,并且只有一个线程的话,程序的其他逻辑就无法执行,从这个程序的角度讲,他没能很好的占有cpu的资源,白白地等待下载的结束。如果开了另外一个线程的话,这个下载线程虽然阻塞掉了,但是别的线程依然可以跑别的逻辑,就可以更充分的占有cpu的资源。有人称同时执行不同的任务为并发。

(TODO? 图1 任务调度示意图)

    同样针对上面第二种问题,我们换个角度表述,我们有一个IO密集型任务,还有一个cpu密集型任务,我们想有效的运行这两个任务,不让IO密集型任务阻塞了cpu密集型任务,所以我们构造了一个任务调度器,当IO密集任务开始从网站上下载大文件的时候,我们把他从调度器上暂时移开,后面再去检查他,让调度器去执行cpu密集任务,在此期间我们可能会时不时的去检查IO任务有没有下载完毕(也可能是被动通知,如软件驱动的中断),如果发现完毕了,调度器可能会暂停当前的cpu密集任务,转而继续执行IO密集任务的后续工作。在多线程环境下,这个调度器就是操作系统,两个任务是两个线程。而协程,就是这个思路,只不过设计的更加极端一点,我们在多线程环境下,几乎不可能手动地控制先执行什么线程,再执行什么线程(只是操作系统的调度工作),而如果我们专门写了一个任务调度器,我们自己实现应用层面的调度算法,就可能实现先执行什么任务,再执行什么任务,什么时候暂停一个任务,什么时候恢复运行这个任务。这个可控的调度机制,就是协程。他的初始目的就是实现任务的并发,只不过想更加精细地控制任务的暂停和继续。协程是种设计思想,线程是计算机实现多任务的一种工程机制,线程可以用于实现协程。

asyncio的模样

    我们先杜撰一段代码应景。

图2 调度器和两个任务

    如图2所示,我们用async def定义了两个函数,一个下载大文件,一个做一些逻辑运算。然后我们实现了一个调度函数,在调度函数里,我们分别用两个任务函数创建了两个任务,加入到asyncio的event loop里面,接着,我们运行这个event loop,这样,两个任务就开始执行起来了。注意,async with和await的时候,都是执行一个异步函数的过程,这个时候,当前任务会主动让出event loop,去后台执行一些网络IO,event loop会选择自己等待队列的任务继续执行。等原来网络IO的任务结束网络IO,他会重新加入到event loop的等待队列,等待其他任务主动让出event loop,被动等待调度。比如self_play在执行await asyncio.sleep(3)的时候会主动让出event loop

    上面我特别强调了主动让出event loop,这是协程的核心思想,如果一个任务没有任何await或async with逻辑,那么它一旦执行,别的任务再也没有机会被调度到。比如,我们如果去掉self_play的await语句,整个event loop将永远被self_play所占用,其他任务再也没有机会执行,整个输出只有左右右手慢动作了。总之,爸爸不给,你不能抢。这一点和朴素的多线程很不一样。

asyncio实现原理推测

    从上面的介绍,我们可以大概猜出,asyncio主要有一个任务调度器(event loop),然后可以用async def定义异步函数作为任务逻辑,通过create_task接口把任务挂到event loop上。event loop的运行过程应该是个不停循环的过程,不停查看等待类别有没有可以执行的任务,如果有的话执行任务,直到碰到await之类的主动让出event loop的函数,如此反复。

(TODO? 图3 event loop调度示意图)

asyncio源码分析

    更进一步的问,evnet loop大致是怎么实现的呢?怎么进行调度的呢?

    我们顺藤摸瓜,在asyncio/base_events.py里面我们看到了create_task的源码实现,代码的关键是Task的构造,传了一个event loop(loop参数)进去,也就是在这个时候,task注册到了event loop上面。注册过程是c实现的(见文末附录1),但本质上都是通过event loop的call_soon()。

图4 create_task的实现

    图5,是run_forever的实现,基本上是不停的在循环,然后每一个循环执行一帧(_run_once)。

图5 run_forever()的实现

    图6是每一帧的代码实现,基本上是在调度队列里找到这一帧应该执行的任务(任务最终注册在event loop的结构是Handle,通过call_soon()实现),直接_run()。

图6 event loop每一帧的逻辑实现

    event loop的call_soon,是注册任务时使用的,字面意思是下一帧执行当前注册的任务。它的本质就是把当前任务封装成Handle,放到_ready里面,如图7所示。

图7 注册任务的最终实现

    调度队列是event驱动形成的,这也是为什么asyncio的核心叫做event loop。这部分代码同样也在_run_once()里面,见图7,这个select就是某种多路复用机制,比如select,epoll和iocp。

图8 event loop处理消息流程

    图8给出了select机制下的selector.select实现,看起来是不是有点熟悉啊。消息处理相关我们后续在常用接口里会再次提到。

图9 select模式下的消息归集

总结一下asyncio的实现思路

    有一个任务调度器event loop,我们可以把需要执行的coroutine打包成task加入到event loop的调度列表里面(以Handle形式)。

    在event loop的每个帧里面,它会检查需要执行那些task,然后运行这些task,可能拿到最终结果,也可能执行一半继续await别的任务,任务之间互相wait,通过回调来把任务串联起来(后面常用接口会继续深入介绍,实现细节见附录2)。

    任务可能会依赖别的IO消息,在每一帧,event loop都会用selector处理相应的消息,执行相应的callback函数。

    我们当前的介绍里,只有一个event loop,这个event loop跑在主线程里面。当然,event loop还可以开线程池处理别的任务,或者,多个线程里执行多个event loop,他们之间还有交互,我们这里不在介绍。   

    单个event loop跑在单个线程有个好处,只要自己不主动await,就会一直占有主线程,换句话说,同步函数一定没有数据冲突(data racing)。对比多线程方案,如果需要处理数据冲突,就需要加锁了,这在很多情况下会降低程序的性能。所以协程这种设计思路,非常适合有多个用户、但是每个用户之间没有共享数据的场景。如果需要实现并行,多开几个进程就行了。

    但是实际上在工程里面,我们很难单用一个线程处理问题,asyncio也不例外,特别在集成别的同步库的时候,可能需要用到别的线程,我们后续介绍。


后续笔记

asyncio常用接口及其意义(实用主义)

如何集成asyncio和同步库(介绍executor线程池对event loop的影响)

为什么异步编程容易犯错(数据冲突)


不适宜骚年的附录

1. Task的构造的c实现

    我们打开男性社交网站,这是event loop实现的核心代码。在这里我们找到了task的c实现_asyncio_Task___init___impl(L1933),它的核心代码执行ask_all_step_soon,间接调用脚本的event loop的call_soon,并且把自己加入到all_task这个全局list(通过register_task,主要是后面索引使用)。

图10 task构造函数实现

2. task执行细节

    task的执行,实现在task_step(L2878)和task_step_impl(L2540)  。其中task_step是asyncio任务执行的核心,对于一个coroutine,每次task_step得到一个结果,然后根据结果判断是否拿到了最终结果,或者需要继续计算等待别的结果,或者把结果扔给自己的waiter。

    python的await都是通过generator实现的,具体的计算在genobject,主要是通过PyEval_EvalFrameEx拿计算结果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容