11.3 异步和事件驱动架构

事件驱动编程会一次监听不同的事件,对于组织程序流程是很好的解决方案,并不需要使用多线程的方法。

考虑这样一个程序,它想要监听一个套接字的连接,并处理收到的连接。有以下三种方式可以解决这个问题。

广告:个人专属 VPN,独立 IP,流量大,速度快,连接稳定,多机房切换,每月最低仅 10 美元

(1)每次有新连接建立时就创建(fork)一个新进程,需要用到multiprocessing这样的模块。

(2)每次有新连接建立时创建一个新线程,需要用到threading这样的模块。

(3)将这个新连接加入事件循环(event loop)中,并在事件发生时对其作出响应。

(现在)众所周知的是,使用事件驱动方法对于监听数百个事件源的场景的效果要好于为每个事件创建一个线程的方式4。这并不意味着二者互不兼容,这只是表明可以通过事件驱动机制摆脱多线程。

我们已经在前面讨论了前面两种选择的优劣,本节只讨论事件驱动机制。

事件驱动架构背后的技术是事件循环的建立。程序调用一个函数,它会一直阻塞直到收到事件。其核心思想是令程序在等待输入输出完成前保持忙碌状态,最基本的事件通常类似于“我有数据就绪可被读取”或者“我可以无阻塞地写入数据”。

在Unix中,用于构建这种事件循环的标准函数是系统调用select(2)或者poll(2)。它们会对几个文件描述符进行监听,并在其中之一准备好读或写时做出响应。

在Python中,这些系统调用通过select模块开放了出来。很容易用它们构造一个事件驱动系统,尽管这显得有些乏味。使用select的基本示例如示例11.4所示。

示例 11.4  使用select的基本示例

import select
import socket

server = socket.socket(socket.AF_INET,
            socket.SOCK_STREAM)
# Never block on read/write operations
server.setblocking(0)

# Bind the socket to the port
server.bind(('localhost', 10000))
server.listen(8)

while True:
  # select() returns 3 arrays containing the object (sockets, files…) that
  # are ready to be read, written to or raised an error
  inputs, outputs, excepts = select.select(
    [server], [], [server])
  if server in inputs:
    connection, client_address = server.accept()
    connection.send("hello!\n")

不久前一个针对这些底层接口的包装器被加入到了Python中,名为asyncore。它还没有被广泛使用,而且演进也不太多。

或者,还有很多其他框架通过更为集成化的方式提供了这类功能,如Twisted(https://twistedmatrix.com/trac/)或者Tornado(http://www.tornadoweb.org/en/stable/)。Twisted多年来在这方面已经成为了事实上的标准。也有一些提供了Python接口的C语言库(如libeventlibev或者libuv)也提供了高效的事件循环。

尽管它们都能解决同样的问题,但不利的一面在于现在选择太多了,而且它们之间大多数不能互操作。而且,它们大多基于回调机制,这意味着在阅读代码时,程序的流程不是很清晰。

gevent(http://www.gevent.org/)或者Greenlet(http://greenlet.readthedocs.org/en/latest/)怎么样呢?它们没有使用回调,但实现的细节很吓人,而且包括一些CPython在x86上的特有的代码以及对标准函数的monkey补丁。如果要长期使用和维护的话实际并非好的选择。

最近,Guido Van Rossum开始致力于一个代号为tulip的解决方案,其记录在PEP 3156中。5这个包的目标就是提供一个标准的事件循环接口。将来,所有的框架和库都将与这个接口兼容,而且将实现互操作。

tulip已经被重命名并被并入了Python 3.4的asyncio包中。如果不打算依赖Python 3.4的话,也可以通过PyPI(https://pypi.python.org/pypi/asyncio)上提供的版本装在Python 3.3上,只需通过运行pip install asyncio即可安装。Victor Stinner已经开始进行移植并将tulip命名为trollius(https://pypi.python.org/pypi/trollius),目标是令其可以兼容Python 2.6及其后续版本。

现在你已经拿到了所有的牌,你肯定会想:那我到底该用什么在事件驱动的应用中构建一个事件循环呢?

在当前的Python开发中,这个问题很难回答。这门语言仍然在转换阶段。截止到本书写作时,还没有什么应用使用了asyncio模块。这意味着用了它很可能面临巨大的挑战。

下面是目前我能给出的一些建议。

 
  • 如果只针对Python 2,asyncio基本不用考虑。对我来说,接下来最好的选择是基于libev的库,如pyevhttps://pypi.python.org/pypi/pyev)。
  • 如果目标是同时支持Python的主要版本(Python 2和Python 3),最好使用能同时支持两个版本的库,如pyev。不过,我必须强烈建议你记住,未来很可能需要迁移到asyncio上。所以有一个最小化的抽象层会很有帮助,并且不要在整个程序中到处产生内部结构对事件库的依赖。如果愿意冒险的话,尝试混合使用asynciotrollius也是个不错的方案。
  • 如果只针对Python 3,那就用asyncio。刚开始会比较痛苦,因为没有太多的例子和文档可以参考,但这是个安全的选择。你将会是先驱。

如示例11.5所示,pyev的接口是很容易掌握的。通过对libev的使用,它通不但支持用于得Io对象,而且支持对子进程的跟踪,计时器、信号量和空闲时的事件回调。libev还可以自动利用polling的最好的接口,如Linux上的epoll(2)或者BSD上的kqueue(2)

示例 11.5 pyev示例

import pyev
import socket

server = socket.socket(socket.AF_INET,
            socket.SOCK_STREAM)
# Never block on read/write operations
server.setblocking(0)

# Bind the socket to the port
server.bind(('localhost', 10000))
server.listen(8)

def server_activity(watcher, revents):
  connection, client_address = server.accept()
  connection.send("hello!\n")
  connection.close()

loop = pyev.default_loop()
watcher = pyev.Io(server, pyev.EV_READ, loop, server_activity)
watcher.start()
loop.start()