考虑使用以下代码读取协议标头:
def readn(sock, n):
buf = ''
while n > len(buf):
newbuf = sock.recv(n - len(buf))
if not newbuf:
raise something
buf += newbuf
return buf
def readmsg(sock):
msgtype = readn(sock, 4).decode('ascii')
size = struct.unpack('!I', readn(sock, 4))
data = readn(sock, size)
return msgtype, size, data
显然,如果你想一次处理多个用户,你不能recv
像这样循环阻塞调用。所以,你可以做什么?
如果你使用线程,你不必对这段代码做任何事情;只需在单独的线程上运行每个客户端,一切都很好。这就像魔术一样。线程的问题是你不能同时运行 5000 个线程而不会使你的调度程序慢下来,分配这么多的堆栈空间以至于你进入交换地狱等等。所以,问题是,我们如何得到没有问题的线程的魔力?
隐式greenlets是解决这个问题的一种方法。基本上,您编写线程代码,它实际上由协作调度程序运行,每次进行阻塞调用时都会中断您的代码。问题在于,这涉及对所有已知的阻塞调用进行猴子修补,并希望您安装的任何库都不会添加任何新的库。
协程就是这个问题的答案。如果您通过在每个阻塞函数调用yield from
之前添加一个显式标记它,则没有人需要猴子补丁任何东西。您仍然需要调用与异步兼容的函数,但不再可能在没有预料到的情况下阻塞整个服务器,并且从您的代码中可以更清楚地看到发生了什么。缺点是幕后的反应器代码必须更复杂......但这是你编写一次的东西(或者,更好的是,零次,因为它来自框架或标准库)。
使用回调,您编写的代码最终会做与使用协程完全相同的事情,但复杂性现在在您的协议代码中。您必须有效地将控制流程从内到外。相比之下,最明显的翻译相当可怕:
def readn(sock, n, callback):
buf = ''
def on_recv(newbuf):
nonlocal buf, callback
if not newbuf:
callback(None, some error)
return
buf += newbuf
if len(buf) == n:
callback(buf)
async_read(sock, n - len(buf), on_recv)
async_read(sock, n, on_recv)
def readmsg(sock, callback):
msgtype, size = None, None
def on_recv_data(buf, err=None):
nonlocal data
if err: callback(None, err)
callback(msgtype, size, buf)
def on_recv_size(buf, err=None):
nonlocal size
if err: callback(None, err)
size = struct.unpack('!I', buf)
readn(sock, size, on_recv_data)
def on_recv_msgtype(buf, err=None):
nonlocal msgtype
if err: callback(None, err)
msgtype = buf.decode('ascii')
readn(sock, 4, on_recv_size)
readn(sock, 4, on_recv_msgtype)
现在,显然,在现实生活中,任何以这种方式编写回调代码的人都应该被枪决;有更好的方法来组织它,比如使用 Futures 或 Deferreds,使用带有方法的类而不是使用 nonlocal 语句以相反顺序定义的一堆局部闭包,等等。
但关键是,没有办法以一种看起来与同步版本相近的方式来编写它。控制流本质上是中心的,协议逻辑是次要的。使用协程,因为控制流总是“向后”的,它在你的代码中根本不明确,协议逻辑就是读写的全部。
话虽如此,有很多地方用回调编写东西的最佳方式比协程(或同步)版本更好,因为代码的重点是将异步事件链接在一起。
如果您通读 Twisted 教程,您会发现让这两种机制很好地协同工作并不难。如果您围绕 Deferred 编写所有内容,则可以自由使用 Deferred 组合函数、显式回调和@inlineCallbacks
-style 协程。在代码的某些部分,控制流很重要,逻辑很琐碎;在其他部分,逻辑很复杂,您不希望它被控制流所掩盖。因此,您可以使用在每种情况下都有意义的任何一个。
事实上,将生成器作为协程与生成器作为迭代器进行比较是值得的。考虑:
def squares(n):
for i in range(n):
yield i*i
def squares(n):
class Iterator:
def __init__(self):
self.i = 0
def __iter__(self):
return self
def __next__(self):
i, self.i = self.i, self.i+1
return i*i
return Iterator(n)
第一个版本隐藏了很多“魔法”——调用之间的迭代器状态在next
任何地方都不是明确的;它隐含在生成器函数的局部框架中。并且每次执行 ayield
时,整个程序的状态都可能在yield
返回之前发生变化。然而,第一个版本显然更加清晰和简单,因为除了产生 N 个平方的操作的实际逻辑之外几乎没有什么可阅读的。
显然,您不希望将您编写的每个程序中的所有状态都放入生成器中。但是完全拒绝使用生成器,因为它们隐藏了状态转换,就像拒绝使用for
循环,因为它隐藏了程序计数器跳转。协程的情况也完全一样。