模板网站与定制开发网站的区别,免费最好网站建设,百度竞价推广代运营公司,别墅建筑设计说明引言1.1. 存储器山存储器山是 Randal Bryant 在《深入理解计算机系统》一书中提出的概念。基于成本、效率的考量#xff0c;计算机存储器被设计成多级金字塔结构#xff0c;塔顶是速度最快、成本最高的 CPU 内部的寄存器(一般几 KB)与高速缓存#xff0c;塔底是成本最低、速…引言1.1. 存储器山存储器山是 Randal Bryant 在《深入理解计算机系统》一书中提出的概念。基于成本、效率的考量计算机存储器被设计成多级金字塔结构塔顶是速度最快、成本最高的 CPU 内部的寄存器(一般几 KB)与高速缓存塔底是成本最低、速度最慢的广域网云存储(如百度云免费 2T )存储器山的指导意义在于揭示了良好设计程序的必要条件是需要有优秀的局部性时间局部性相同时间内访问同一地址次数越多则时间局部性表现越佳空间局部性下一次访问的存储器地址与上一次的访问过的存储器地址位置邻近1.2. cpu的时间观操作真实延迟CPU体验执指0.38ns1s读L1缓存0.5ns1.3s分支纠错5ns13s读L2缓存7ns18.2s加解互斥锁25ns1min 5s内存寻址100ns4min 20s上下文切换/系统调用1.5us1h 5min1Gbps网络传输2KB数据20us14.4h从RAM取1M数据块250us7.5dayPing单一IDC主机500us15day从SSD读1M数据1ms1month从硬盘读1M数据20ms20monthPing不同城市主机150 ms12.5year虚拟机重启4s300year服务器重启5min25000year我们将一个普通的 2.6GHz 的 CPU 的延迟时间放大到人能体验的尺度上(数据来自微信公众号 驹说码事)在存储器顶层执行单条寄存器指令的时间为1秒钟从第五层磁盘读 1MB 数据却需要一年半ping 不同的城域网主机网络包需要走 12.5 年。如果程序发送了一个 HTTP 包后便阻塞在同步等待响应的过程上计算机不得不傻等 12 年后的那个响应再处理别的事情低下的硬件利用率必然导致低下的程序效率。1.3. 同步编程从以上数据可以看出内存数据读写、磁盘寻道读写、网卡读写等操作都是 I/O 操作同步程序的瓶颈在于漫长的 I/O 等待想要提高程序效率必须减少 I/O 等待时间从提高程序的局部性着手。同步编程的改进方式有多进程、多线程但对于 c10k 问题都不是良好的解决方案多进程的方式存在操作系统可调度进程数量上限较低进程间上下文切换时间过长进程间通信较为复杂。而 Python 的多线程方式由于存在众所周知的 GIL 锁性能提升并不稳定仅能满足成百上千规模的 I/O 密集型任务多线程还有一个缺点是由操作系统进行抢占式调度存在竞态条件可能需要引入了锁与队列等保障原子性操作的工具。1.4. 异步编程说到异步非阻塞调用目前的代名词都是epoll 与 kqueueselect/poll 由于效率问题基本已被取代。epoll 是04年 Linux2.6 引入内核的一种 I/O 事件通知机制它的作用是将大量的文件描述符托管给内核内核将最底层的 I/O 状态变化封装成读写事件这样就避免了由程序员去主动轮询状态变化的重复工作程序员将回调函数注册到 epoll 的状态上当检测到相对应文件描述符产生状态变化时就进行函数回调。事件循环是异步编程的底层基石。上图是简单的EventLoop的实现原理用户创建了两个socket连接将系统返回的两个文件描述符fd3、fd4通过系统调用在epoll上注册读写事件当网卡解析到一个tcp包时内核根据五元组找到相应到文件描述符自动触发其对应的就绪事件状态并将该文件描述符添加到就绪链表中。程序调用epoll.poll()返回可读写的事件集合。对事件集合进行轮询调用回调函数等一轮事件循环结束循环往复。epoll 并非银弹从图中可以观察到如果用户关注的层次很低直接操作epoll去构造维护事件的循环从底层到高层的业务逻辑需要层层回调造成callback hell并且可读性较差。所以这个繁琐的注册回调与回调的过程得以封装并抽象成EventLoop。EventLoop屏蔽了进行epoll系统调用的具体操作。对于用户来说将不同的I/O状态考量为事件的触发只需关注更高层次下不同事件的回调行为。诸如libev, libevent之类的使用C编写的高性能异步事件库已经取代这部分琐碎的工作。在Python框架里一般会见到的这几种事件循环libevent/libev: Gevent(greenlet前期libevent后期libev)使用的网络库广泛应用tornado: tornado框架自己实现的IOLOOPpicoev: meinheld(greenletpicoev)使用的网络库小巧轻量相较于libevent在数据结构和事件检测模型上做了改进所以速度更快。但从github看起来已经年久失修用的人不多。uvloop: Python3时代的新起之秀。Guido操刀打造了asyncio库asyncio可以配置可插拔的event loop但需要满足相关的API要求uvloop继承自libuv将一些低层的结构体和函数用Python对象包装。目前Sanic框架基于这个库1.5. 协程EventLoop简化了不同平台上的事件处理但是处理事件触发时的回调依然很麻烦响应式的异步程序编写对程序员的心智是一项不小的麻烦。因此协程被引入来替代回调以简化问题。协程模型主要在在以下方面优于回调模型以近似同步代码的编程模式取代异步回调模式真实的业务逻辑往往是同步线性推演的因此这种同步式的代码写起来更加容易。底层的回调依然是callback hell但这部分脏活累活已经转交给编译器与解释器去完成程序员不易出错。异常处理更加健全可以复用语言内的错误处理机制回调方式。而传统异步回调模式需要自己判定成功失败错误处理行为复杂化。上下文管理简单化回调方式代码上下文管理严重依赖闭包不同的回调函数之间相互耦合割裂了相同的上下文处理逻辑。协程直接利用代码的执行位置来表示状态而回调则是维护了一堆数据结构来处理状态。方便处理并发行为协程的开销成本很低每一个协程仅有一个轻巧的用户态栈空间。1.6. EventLoop与协程的发展史04年event-driven 的 nginx 诞生并快速传播06年以后从俄语区国家扩散到全球。同时期EventLoop 变得具象化与多元化相继在不同的编程语言实现。近十年以来后端领域内古老的子例程与事件循环得到结合协程(协作式子例程)快速发展并也革新与诞生了一些语言比如 golang 的 goroutineluajit 的 coroutinePython 的 gevent,erlang 的 processscala 的 actor 等。就不同语言中面向并发设计的协程实现而言Scala 与 Erlang 的 Actor 模型、Golang 中的 goroutine 都较 Python 更为成熟不同的协程使用通信来共享内存优化了竞态、冲突、不一致性等问题。然而根本的理念没有区别都是在用户态通过事件循环驱动实现调度。由于历史包袱较少后端语言上的各种异步技术除 Python Twisted 外基本也没有 callback hell 的存在。其他的方案都已经将 callback hell 的过程进行封装交给库代码、编译器、解释器去解决。有了协程有了事件循环库传统的 C10K 问题已经不是挑战并已经上升到了 C1M 问题。2. GeventPython2 时代的协程技术主要是 Gevent另一个 meinheld 比较小众。Gevent 有褒有贬负面观点认为它的实现不够 Pythonic脱离解释器独自实现了黑盒的调度器monkey patch 让不了解的用户产生混淆。正面观点认为正是这样才得以屏蔽所有的细节简化使用难度。Gevent 基于 Greenlet 与 Libevgreenlet 是一种微线程或者协程在调度粒度上比 PY3 的协程更大。greenlet 存在于线程容器中其行为类似线程有自己独立的栈空间不同的 greenlet 的切换类似操作系统层的线程切换。greenlet.hub 也是一个继承于原生 greenlet 的对象也是其他 greenlet 的父节点它主要负责任务调度。当一个 greenlet 协程执行完部分例程后到达断点通过 greenlet.switch() 向上转交控制权给 hub 对象hub 执行上下文切换的操作从寄存器、高速缓存中备份当前 greenlet 的栈内容到内存中并将原来备份的另一个 greenlet 栈数据恢复到寄存器中。hub 对象内封装了一个 loop 对象loop 负责封装 libev 的相关操作并向上提供接口所有 greenlet 在通过 loop 驱动的 hub 下被调度。3. 从yield到async/await3.1. 生成器的进化在 Python2.2 中第一次引入了生成器生成器实现了一种惰性、多次取值的方法此时还是通过 next 构造生成迭代链或 next 进行多次取值。直到在 Python2.5 中yield 关键字被加入到语法中这时生成器有了记忆功能下一次从生成器中取值可以恢复到生成器上次 yield 执行的位置。之前的生成器都是关于如何构造迭代器在 Python2.5 中生成器还加入了 send 方法与 yield 搭配使用。我们发现此时生成器不仅仅可以 yield 暂停到一个状态还可以往它停止的位置通过 send 方法传入一个值改变其状态。举一个简单的示例主要熟悉 yield 与 send 与外界的交互流程def jump_range(up_to):step 0while step up_to:jump yield stepprint(jump, jump)if jump isNone:jump 1step jumpprint(step, step)if __name__ __main__:iterator jump_range(10)print(next(iterator)) # 0print(iterator.send(4)) # jump4; step4; 4print(next(iterator)) # jump None; step5; 5print(iterator.send(-1)) # jump -1; step4; 4在 Python3.3 中生成器又引入了 yield from 关键字yield from 实现了在生成器内调用另外生成器的功能可以轻易的重构生成器比如将多个生成器连接在一起执行。def gen_3():yield3def gen_234():yield2yieldfrom gen_3()yield4def main():yield1yieldfrom gen_234()yield5for element in main():print(element) # 1,2,3,4,5从图中可以看出 yield from 的特点。使用 itertools.chain 可以以生成器为最小组合子进行链式组合使用 itertools.cycle 可以对单独一个生成器首尾相接构造一个循环链。使用 yield from 时可以在生成器中从其他生成器 yield 一个值这样不同的生成器之间可以互相通信这样构造出的生成链更加复杂但生成链最小组合子的粒度却精细至单个 yield 对象。3.2. 短暂的asynico.coroutine 与yield from有了Python3.3中引入的yield from 这项工具Python3.4 中新加入了asyncio库并提供了一个默认的event loop。Python3.4有了足够的基础工具进行异步并发编程。并发编程同时执行多条独立的逻辑流每个协程都有独立的栈空间即使它们是都工作在同个线程中的。以下是一个示例代码import asyncioimport aiohttpasyncio.coroutinedef fetch_page(session, url):response yieldfrom session.get(url)if response.status 200:text yieldfrom response.text()print(text)loop asyncio.get_event_loop()session aiohttp.ClientSession(looploop)tasks [asyncio.ensure_future(fetch_page(session, http://bigsec.com/products/redq/)),asyncio.ensure_future(fetch_page(session, http://bigsec.com/products/warden/))]loop.run_until_complete(asyncio.wait(tasks))session.close()loop.close()在 Python3.4 中asyncio.coroutine 装饰器是用来将函数转换为协程的语法这也是 Python 第一次提供的生成器协程 。只有通过该装饰器生成器才能实现协程接口。使用协程时你需要使用 yield from 关键字将一个 asyncio.Future 对象向下传递给事件循环当这个 Future 对象还未就绪时该协程就暂时挂起以处理其他任务。一旦 Future 对象完成事件循环将会侦测到状态变化会将 Future 对象的结果通过 send 方法方法返回给生成器协程然后生成器恢复工作。在以上的示例代码中首先实例化一个 eventloop并将其传递给 aiohttp.ClientSession 使用这样 session 就不用创建自己的事件循环。此处显式的创建了两个任务只有当 fetch_page 取得 api.bigsec.com 两个 url 的数据并打印完成后所有任务才能结束然后关闭 session 与 loop释放连接资源。当代码运行到 response yield from session.get(url)处fetch_page 协程被挂起隐式的将一个 Future 对象传递给事件循环只有当 session.get() 完成后该任务才算完成。session.get() 内部也是协程其数据传输位于在存储器山最慢的网络层。当 session.get 完成时取得了一个 response 对象再传递给原来的 fetch_page 生成器协程恢复其工作状态。为了提高速度此处 get 方法将取得 http header 与 body 分解成两次任务减少一次性传输的数据量。response.text() 即是异步请求 http body。使用 dis 库查看 fetch_page 协程的字节码GET_YIELD_FROM_ITER 是 yield from 的操作码:In [4]: import disIn [5]: dis.dis(fetch_page)0 LOAD_FAST 0 (session)2 LOAD_ATTR 0 (get)4 LOAD_FAST 1 (url)6 CALL_FUNCTION 18 GET_YIELD_FROM_ITER10 LOAD_CONST 0 (None)12 YIELD_FROM14 STORE_FAST 2 (response)16 LOAD_FAST 2 (response)18 LOAD_ATTR 1 (status)20 LOAD_CONST 1 (200)22 COMPARE_OP 2 ()24 POP_JUMP_IF_FALSE 4826 LOAD_FAST 2 (response)28 LOAD_ATTR 2 (text)30 CALL_FUNCTION 032 GET_YIELD_FROM_ITER34 LOAD_CONST 0 (None)36 YIELD_FROM38 STORE_FAST 3 (text)40 LOAD_GLOBAL 3 (print)42 LOAD_FAST 3 (text)44 CALL_FUNCTION 146 POP_TOP 48 LOAD_CONST 0 (None)50 RETURN_VALUE3.3. async与 await关键字Python3.5 中引入了这两个关键字用以取代 asyncio.coroutine 与 yield from从语义上定义了原生协程关键字避免了使用者对生成器协程与生成器的混淆。这个阶段(3.0-3.4)使用 Python 的人不多因此历史包袱不重可以进行一些较大的革新。await 的行为类似 yield from但是它们异步等待的对象并不一致yield from 等待的是一个生成器对象而await接收的是定义了__await__方法的 awaitable 对象。在 Python 中协程也是 awaitable 对象collections.abc.Coroutine 对象继承自 collections.abc.Awaitable。因此将上一小节的示例代码改写成import asyncioimport aiohttpasyncdef fetch_page(session, url):response await session.get(url)if response.status 200:text await response.text()print(text)loop asyncio.get_event_loop()session aiohttp.ClientSession(looploop)tasks [asyncio.ensure_future(fetch_page(session, http://bigsec.com/products/redq/)),asyncio.ensure_future(fetch_page(session, http://bigsec.com/products/warden/))]loop.run_until_complete(asyncio.wait(tasks))session.close()loop.close()从 Python 语言发展的角度来说async/await 并非是多么伟大的改进只是引进了其他语言中成熟的语义协程的基石还是在于 eventloop 库的发展以及生成器的完善。从结构原理而言asyncio 实质担当的角色是一个异步框架async/await 是为异步框架提供的 API因为使用者目前并不能脱离 asyncio 或其他异步库使用 async/await 编写协程代码。即使用户可以避免显式地实例化事件循环比如支持 asyncio/await 语法的协程网络库 curio但是脱离了 eventloop 如心脏般的驱动作用async/await 关键字本身也毫无作用。4. async/await的使用4.1. Future不用回调方法编写异步代码后为了获取异步调用的结果引入一个 Future 未来对象。Future 封装了与 loop 的交互行为add_done_callback 方法向 epoll 注册回调函数当 result 属性得到返回值后会运行之前注册的回调函数向上传递给 coroutine。但是每一个角色各有自己的职责用 Future 向生成器 send result 以恢复工作状态并不合适Future 对象本身的生存周期比较短每一次注册回调、产生事件、触发回调过程后工作已经完成。所以这里又需要在生成器协程与 Future 对象中引入一个新的对象 Task对生成器协程进行状态管理。4.2. TaskTask顾名思义是维护生成器协程状态处理执行逻辑的的任务Task 内的_step 方法负责生成器协程与 EventLoop 交互过程的状态迁移:向协程 send 一个值恢复其工作状态协程运行到断点后得到新的未来对象再处理 future 与 loop 的回调注册过程。4.3. Loop事件循环的工作方式与用户设想存在一些偏差理所当然的认知应是每个线程都可以有一个独立的 loop。但是在运行中在主线程中才能通过 asyncio.get_event_loop() 创建一个新的 loop而在其他线程时使用 get_event_loop() 却会抛错正确的做法应该是 asyncio.set_event_loop() 进行当前线程与 loop 的显式绑定。由于 loop 的运作行为并不受 Python 代码的控制所以无法稳定的将协程拓展到多线程中运行。协程在工作时并不了解是哪个 loop 在对其调度即使调用 asyncio.get_event_loop() 也不一定能获取到真正运行的那个 loop。因此在各种库代码中实例化对象时都必须显式的传递当前的 loop 以进行绑定。4.3. 另一个FuturePython 里另一个 Future 对象是 concurrent.futures.Future与 asyncio.Future 互不兼容但容易产生混淆。concurrent.futures 是线程级的 Future 对象当使用 concurrent.futures.Executor 进行多线程编程时用于在不同的 thread 之间传递结果。4.4. 现阶段asyncio生态发展的困难由于这两个关键字在2014年发布的Python3.5中才被引入发展历史较短在Python2与Python3割裂的大环境下生态环境的建立并不完善对于使用者来说希望的逻辑是引入一个库然后调用并获取结果并不关心第三方库的内部逻辑。然而使用协程编写异步代码时需要处理与事件循环的交互。对于异步库来说其对外封装性并不能达到同步库那么高。异步编程时用户通常只会选择一个第三方库来处理所有HTTP逻辑。但是不同的异步实现方法不一致互不兼容分歧阻碍了社区壮大异步代码虽然快但不能阻塞一旦阻塞整个程序失效。使用多线程或多进程的方式将调度权交给操作系统未免不是一种自我保护有疑问加站长微信联系(非本文作者)