epoll、异步、并发

关于异步、并发相关的笔记与资料汇总。

epoll:

一般情况下,异步非阻塞IO模型性能是远高于同步阻塞IO模型的,可以参考nginx与apache性能的对比。

貌似现在主流的异步非阻塞IO的实现都是基于单线程的事件循环来实现的,底层用的是epoll(windows下是iocp,mac下是kqueue)。异步在linux下主要有三种实现:select,poll,epoll。

并发(不是并行)编程目前有四种方式,多进程,多线程,异步,和协程。关于协程,可以参考greenlet,stackless,gevent,eventlet等的实现。

涉及到底层原理和实现,水平马上就暴露了,实在有限,道理是看懂一些了,还是没弄懂具体是怎么回事。不过对于轮询和事件注册大概是弄懂了,select和poll大概是对应轮询,epoll对应事件注册和回调,用小明和宿管大妈找人的例子比较形象易懂。

epoll底层用红黑树和链表存储socket和就绪事件,用定时循环处理事件。实现事件循环和多路复用IO。

以上对epoll的现在的认知,可能是有问题的,还没从根本上弄清原理。

同步编程:

同步编程的改进方式有多进程、多线程,但对于 c10k 问题都不是良好的解决方案,多进程的方式存在操作系统可调度进程数量上限较低,进程间上下文切换时间过长,进程间通信较为复杂。

而 Python 的多线程方式,由于存在众所周知的GIL锁,性能提升并不稳定,仅能满足成百上千规模的 I/O 密集型任务,多线程还有一个缺点是由操作系统进行抢占式调度存在竞态条件,可能需要引入了锁与队列等保障原子性操作的工具

异步编程:

说到异步非阻塞调用,目前的代名词都是 epoll 与 kqueue,select/poll 由于效率问题基本已被取代。

epoll 是04年 Linux2.6 引入内核的一种 I/O 事件通知机制,它的作用是将大量的文件描述符托管给内核,内核将最底层的 I/O 状态变化封装成读写事件,这样就避免了由程序员去主动轮询状态变化的重复工作,程序员将回调函数注册到 epoll 的状态上,当检测到相对应文件描述符产生状态变化时,就进行函数回调

事件循环是异步编程的底层基石。

epoll繁琐的注册回调与回调的过程得以封装,并抽象成EventLoop。EventLoop屏蔽了进行epoll系统调用的具体操作。对于用户来说,将不同的I/O状态考量为事件的触发,只需关注更高层次下不同事件的回调行为。诸如libev, libevent之类的使用C编写的高性能异步事件库已经取代这部分琐碎的工作。

在Python框架里一般会见到的这几种事件循环:

  • libevent/libev: Gevent(greenlet+前期libevent,后期libev)使用的网络库,广泛应用;
  • tornado: tornado框架自己实现的IOLOOP;
  • uvloop: Python3时代的新起之秀。Guido操刀打造了asyncio库,asyncio可以配置可插拔的event loop,但需要满足相关的API要求,uvloop继承自libuv,将一些低层的结构体和函数用Python对象包装。目前Sanic框架基于这个库

协程:

Python中解决IO密集型任务(打开多个网站)的方式有很多种,比如多进程、多线程。但理论上一台电脑中的线程数、进程数是有限的,而且进程、线程之间的切换也比较消耗资源、浪费时间。所以就出现了“协程”的概念。协程允许一个执行过程A阻塞时挂起,然后转到执行过程B,在适当的时候再一次回调回来,有点类似于多线程。但协程有以下优势:

  • 协程的数量理论上可以是无限个,而且没有线程之间的上下文切换动作,执行效率比线程高。开销低得多
  • 协程不需要“锁”机制,即不需要lock和release过程,因为所有的协程都在一个线程中。

EventLoop简化了不同平台上的事件处理,但是处理事件触发时的回调依然很麻烦,响应式的异步程序编写对程序员的心智是一项不小的麻烦。

因此,协程被引入来替代回调以简化问题。协程模型主要在在以下方面优于回调模型:

  • 以近似同步代码的编程模式取代异步回调模式,真实的业务逻辑往往是同步线性推演的,因此,这种同步式的代码写起来更加容易。底层的回调依然是callback hell,但这部分脏活累活已经转交给编译器与解释器去完成,程序员不易出错。
  • 异常处理更加健全,可以复用语言内的错误处理机制,回调方式。而传统异步回调模式需要自己判定成功失败,错误处理行为复杂化。
  • 上下文管理简单化,回调方式代码上下文管理严重依赖闭包,不同的回调函数之间相互耦合,割裂了相同的上下文处理逻辑。协程直接利用代码的执行位置来表示状态,而回调则是维护了一堆数据结构来处理状态。
  • 方便处理并发行为,协程的开销成本很低,每一个协程仅有一个轻巧的用户态栈空间。

EventLoop与协程的发展史:

04年,event-driven 的 nginx 诞生并快速传播,06年以后从俄语区国家扩散到全球。同时期,EventLoop 变得具象化与多元化,相继在不同的编程语言实现。

近十年以来,后端领域内古老的子例程与事件循环得到结合,协程(协作式子例程)快速发展,并也革新与诞生了一些语言,比如 golang 的 goroutine,luajit 的 coroutine,Python 的 gevent,erlang 的 process,scala 的 actor 等。

就不同语言中面向并发设计的协程实现而言,Scala 与 Erlang 的 Actor 模型、Golang 中的 goroutine 都较 Python 更为成熟,不同的协程使用通信来共享内存,优化了竞态、冲突、不一致性等问题。然而,根本的理念没有区别,都是在用户态通过事件循环驱动实现调度。

由于历史包袱较少,后端语言上的各种异步技术除 Python Twisted 外基本也没有 callback hell 的存在。其他的方案都已经将 callback hell 的过程进行封装,交给库代码、编译器、解释器去解决。

有了协程,有了事件循环库,传统的 C10K 问题已经不是挑战并已经上升到了 C1M 问题。

asyncio:

Python3.3以后到Python3.6,从生成器发展而来,从yield from到async/await,asyncio 比其他协程的实现和使用更加优雅。从 Python 语言发展的角度来说,async/await 并非是多么伟大的改进,只是引进了其他语言中成熟的语义,协程的基石还是在于 eventloop 库的发展,以及生成器的完善。

从结构原理而言,asyncio 实质担当的角色是一个异步框架,async/await 是为异步框架提供的 API,因为使用者目前并不能脱离 asyncio 或其他异步库使用 async/await 编写协程代码。即使用户可以避免显式地实例化事件循环,比如支持 asyncio/await 语法的协程网络库 curio,但是脱离了 eventloop 如心脏般的驱动作用,async/await 关键字本身也毫无作用。

阅读和参考资料: