虽然第一眼看上去不是特别明显,但实际上传统的事件驱动编程伴随着典型的问题就是衍生自那个协程占据主循环问题。
在典型的事件驱动平台下,一个外部的实体向我们程序中所谓的事件循环或运行循环生成事件。这里,我们的代码很明显不是主循环。我们的程序变成了事件循环的附属品,使得我们的程序成为了一组无需任何显式关联的、相互独立的事件处理程序的集合。
再举一个更加具体的例子,假设有一个与 libuv
类似的异步 I/O
库,该库中有四个与我们的示例有关的函数:
lib.runloop(); lib.readline(stream, callback); lib.writeline(stream, line, callback); lib.stop();
第一个函数运行事件循环,在其中处理所有发生的事件并调用对应的回调函数。一个典型的事件驱动程序初始化某些机制然后调用这个函数,这个函数就变成了应用的主循环。第二个函数指示库从指定的流中读取一行,并在读取完成后带着读取的结果调用指定的回调函数。第三个函数与第二个函数类似,只是该函数写入一行。最后一个函数打破事件循环,通常用于结束程序。
local cmdQueue = {} -- 挂起操作的队列 local lib = {} function lib.readline (stream, callback) local nextCmd = function () callback(stream:read()) end table.insert(cdmQueue, nextCmd) end function lib.writeline(stream, line, callback) local nextCmd = function () callback(stream:write(line)) end table.insert(cmdQueue, nextCmd) end function lib.stop() table.insert(cmdQueue, "stop") end function lib.runloop () while true do local nextCmd = table.remove(cmdQueue, 1) if nextCmd == "stop" then break else nextCmd() -- 进行下一个操作 end end end return lib
上述代码是一种简单而丑陋的实现。该程序的“事件队列”实际上是一个由挂起操作组成的列表,当这些操作被异步调用时会产生事件。尽管很丑陋,但该程序还是完成了之前我们提到的功能,也使得我们无需使用真实的异步库就可以测试接下来的例子。
现在,让我们编写一个使用这个库的简单程序,这个程序把输入流中的所有行读取到一个表中,然后再逆序将其写到输出流中。如果使用同步 I/O
,那么代码可能如下:
local t = {} local inp = io.input() -- 输入流 local out = io.output() -- 输出流 for line in inp:lines() do t[#t + 1] = line end for i = #t, 1, -1 do out:write(t[i], "\n") end
现在,让我们再使用异步 I/O
库按照事件驱动的方式重写这个程序:
local lib = require "async-lib" local t = {} local inp = io.input() local out = io.output() local i -- 写入行的事件处理函数 local function putline() i = i -1 if i == 0 then -- 没有行了? lib.stop() -- 结束主循环 else -- 写一行然后准备下一行 lib.writeline(out, t[i] .. "\n", putline) end end -- 读取行的事件处理函数 local function getline (line) if line then -- 不是EOF? t[#t + 1] = line -- 保存行 lib.readline(inp, getline) -- 读取下一行 else -- 文件结束 i = #t + 1 -- 准备写入循环 putline() -- 进入写入循环 end end lib.readline(inp, getline) -- 读取第一行 lib.runloop() -- 运行主循环点击复制复制失败已复制
作为一种典型的事件驱动场景,由于主循环位于库中,因此所有的循环都消失了,这些循环被以事件区分的递归调用所取代。尽管我们可以通过使用闭包以后续传递风格进行改进,但仍然不能编写我们自己的循环。如果要这么做,那么必须通过递归来重写。
协程可以让我们使用事件循环来简化循环的代码,其核心思想是使用协程运行主要代码,即在每次调用库时将回调函数设置为唤醒协程的函数然后让出执行权。如下所示的代码使用这种思想实现了一个在异步 I/O
库上运行传统同步代码的示例:
local lib = require "async-lib" function run (code) local co = coroutine.wrap(function() code() lib.stop() end) co() lib.runloop() end function putline(stream, line) local co = coroutine.running() local callback = (function () coroutine.resume(co) end) lib.witeline(stream, line, callback) coroutine.yield() end function getline(stream, line) local co = coroutine.running() local callback = (function (l) coroutine.resume(co, l) end) lib.readline(stream, callback) local line = coroutine.yield() return line end
顾名思义, run
函数运行通过参数传入的同步代码。该函数首先创建一个协程来运行指定的代码,并在完成后停止事件循环。然后,该函数唤醒协程(协程会在第一次 I/O
操作时挂起),进入事件循环。
函数 getline
和 putline
模拟了同步 I/O
。正如之前强调的,这两个函数都调用了恰当的异步函数,这些异步函数被当做唤醒调用协程的回调函数传入。之后,异步函数挂起,然后将控制权返回给事件循环。一旦异步操作完成,事件循环就会调用回调函数来唤醒触发异步函数的协程。
注意
函数 coroutine.running
用来访问调用协程。
使用这个库,我们就可以在异步库上运行同步代码了。如下所示的示例再次实现了逆序行的例子:
run(function () local t = {} local inp = io.input() local out = io.output() while true do local line = getline(inp) if not line then break end t[#t + 1] = line end for i = #t, 1, -1 do putline(out, t[i] .. "\n") end end)
除了使用了 get/putline
来进行 I/O
操作和运行在 run
以内,上述代码与之前的同步示例等价。在同步代码结构的外表之下,程序其实是以事件驱动模式运行的。同时,该程序与以更典型的事件驱动风格编写的程序的其他部分也完全兼容。