一、简介1.内存山内存山是RandalBryant在《深入理解计算机系统》一书中提出的一个概念。基于成本和效率的考虑,计算机内存被设计成多层金字塔结构。塔顶是最快最便宜的CPU内部寄存器(一般几KB)和缓存,塔底是成本最低,最慢的WAN云存储(比如百度云免费2T)的指导意义是为了揭示一个设计良好的程序的必要条件是具有优良的局部性:时间局部性:同一地址在同一时间段内被访问的次数越多,时间局部性的性能越好;空间局部性:下一次访问的内存地址与上一次访问的内存地址相邻;2.cpu时间查看我们把普通2.6GHzCPU的延迟时间放大到人类可以体验的尺度上(数据来自微信公众号巨硕代码世):在最顶层执行单个寄存器指令的时间内存为1秒;从第五层磁盘读取1MB数据需要一年半的时间;ping不一样对于城域网主机,网络包需要走12.5年。如果程序在发送一个HTTP数据包后同步等待响应的过程中被阻塞,那么计算机要等待12年才能响应才能处理其他事情,硬件利用率低下必然导致程序效率低下。3、同步编程从上面的数据可以看出,内存数据读写、磁盘寻道读写、网卡读写等操作都是I/O操作。同步程序的瓶颈在于长时间的I/O等待。要想提高程序效率必须减少I/O等待时间,从提高程序的局部性入手。同步编程的改进方法有多进程、多线程,但是对于c10k的问题并没有很好的解决办法。多进程方式操作系统可调度的进程数上限较低,进程间上下文切换时间过长,进程间通信比较慢。复杂的。但是由于Python的多线程方式中众所周知的GIL锁,性能提升并不稳定,只能满足成百上千的I/O密集型任务。多线程的另一个缺点是操作系统执行抢占式调度。存在竞争条件,可能需要引入锁、队列等工具来保证原子操作。4、异步编程说到异步非阻塞调用,目前的代名词是epoll和kqueue,select/poll因为效率问题已经基本被替代。epoll是2004年Linux2.6引入内核的I/O事件通知机制,其作用是将大量的文件描述符寄存在内核中,内核将顶层的I/O状态变化封装为read和写事件,避免程序员主动轮询状态变化的重复工作。程序员将回调函数注册到epoll的状态,当检测到对应文件描述符的状态变化时,进行函数回调。事件循环是异步编程的底层基石。上图展示了一个简单的EventLoop的实现原理。用户创建两个socket连接,通过系统返回的两个文件描述符fd3和fd4在epoll上注册读写事件;当网卡解析一个tcp包时,内核根据五元组找到对应的文件描述符,自动触发其对应的就绪事件状态,并将该文件描述符加入就绪列表。程序调用epoll.poll()返回一个可以读写的事件集合。轮询事件集合,调用回调函数等。一轮事件循环结束,周而复始。epoll不是灵丹妙药。从图中可以观察到,如果用户关注度很低,直接运行epoll构造一个维护事件的循环。从底层到顶层的业务逻辑需要逐层回调,造成回调地狱,可读性差。因此,将注册回调和回调这个繁琐的过程封装抽象成EventLoop。EventLoop屏蔽了进行epoll系统调用的具体操作。对于用户而言,将不同的I/O状态视为事件触发,只需要在更高层次上关注不同事件的回调行为即可。libev、libevent等用C语言编写的高性能异步事件库取代了这种繁琐的工作。这几种事件循环在Python框架中普遍可见:libevent/libev:Gevent使用的网络库(前期greenlet+libevent,后期libev),应用广泛;tornado:由tornado框架本身实现的IOLOP;picoev:meinheld(greenlet+picoev使用的网络库)小巧轻便。与libevent相比,改进了数据结构和事件检测模型,因此速度更快。但是从github上看,好像年久失修了,用的人不多。uvloop:Python3时代的新人。Guido创建了asyncio库。Asyncio可以配置一个可插拔的事件循环,但是需要满足相关的API要求。uvloop继承自libuv,并用Python对象包装了一些低级结构和函数。目前的Sanic框架都是基于这个库5.协程EventLoop简化了不同平台的事件处理,但是在事件触发时处理回调还是很麻烦。响应式的异步程序编写对于程序员的脑子来说是个不小的麻烦。因此引入了协程来代替回调来简化事情。协程模型主要在以下几个方面优于回调模型:异步回调模式被类似于同步代码的编程模式所取代。真正的业务逻辑往往是同步线性推导的。因此,这种同步代码更容易编写。回调底层依然是回调地狱,但这部分脏活累活都交给了编译器和解释器来完成,程序员出错的可能性更小。异常处理更加完善,可以复用语言中的错误处理机制和回调方法。但传统的异步回调方式需要自行判断成功或失败,错误处理行为复杂。上下文管理得到简化。回调模式代码上下文管理在很大程度上依赖于闭包。不同的回调函数相互耦合,分离了相同的上下文处理逻辑。协程直接用代码的执行位置来表示状态,而回调则维护一堆数据结构来处理状态。方便处理并发行为,协程的开销成本很低,每个协程只有一个轻量级用户态栈空间。6.EventLoop与协程的发展历程2004年,事件驱动的nginx诞生并迅速传播开来。2006年以后,从俄语国家传到世界各地。同时,EventLoop也变得具体化和多样化,并被不同的编程语言实现。近十年来,后端领域古老的子程序和事件循环相结合,协程(协同子程序)得到快速发展,一些语言也被创新诞生,比如golang的goroutine,luajit的coroutine,Python的gevent,erlang的process,scala的actor等。在不同语言面向并发设计的协程实现方面,Scala和Erlang中的actor模型,Golang中的goroutine比Python成熟。不同的协程使用通信来共享内存,从而优化竞争条件、冲突和不一致等问题。但是在基本概念上没有区别,都是在用户态通过事件循环驱动实现调度。由于历史包袱较轻,后端语言上的各种异步技术除了PythonTwisted外基本没有回调地狱。其他解决方案已经将回调地狱的过程封装起来,交给库代码、编译器、解释器来解决。有了协程和事件循环库,传统的C10K问题不再是挑战,已经上升为C1M问题。2、GeventPython2时代的协程技术主要是Gevent,其他的比较少。Gevent既有赞扬也有批评。消极的观点是它的实现不够Pythonic。它独立于解释器实现了一个黑盒调度器。猴子补丁让不了解它的用户感到困惑。积极的观点是这样可以屏蔽所有细节,简化使用难度。Gevent基于Greenlet和Libev。Greenlet是一种微线程或者协程,其调度粒度比PY3更大。一个greenlet存在于一个线程容器中,其行为类似于一个线程,有自己独立的栈空间。不同greenlet的切换类似于操作系统层的线程切换。greenlet.hub也是从原来的greenlet继承而来的对象,也是其他greenlet的父节点。它主要负责任务调度。当一个greenlet协程在执行部分例程后遇到断点时,通过greenlet.switch()将控制权交给hub对象,hub执行上下文切换操作:从寄存器中备份当前greenlet的栈内容并缓存到内存中,并恢复另外一个greenlet栈数据,原来备份到寄存器中。循环对象封装在集线器对象中。loop负责封装libev的相关操作,向上提供接口。所有greenlets都被调度在由循环驱动的hub下。3.从yield到async/await1。生成器的演变Python2.2中首次引入了生成器。生成器实现了一种惰性和多值获取的方法。这时候,它们仍然是通过next构造迭代链或者next为多个值生成的。直到Python2.5,yield关键字才被添加到语法中。此时生成器具有记忆功能,下次从生成器中传出的值可以恢复到生成器上次执行yield的位置。之前的生成器都是关于如何构造迭代器的。在Python2.5中,generators还增加了send方法,与yield配合使用。我们发现,此时generator不仅可以暂停到yield的状态,还可以通过send方法传入一个值,改变其停止点的状态。举个简单的例子,主要是熟悉yield和send与外界的交互过程:defjump_range(up_to):step=0whilestep
