事件驱动式编程

简介: 事件驱动式编程

虽然第一眼看上去不是特别明显,但实际上传统的事件驱动编程伴随着典型的问题就是衍生自那个协程占据主循环问题。


在典型的事件驱动平台下,一个外部的实体向我们程序中所谓的事件循环或运行循环生成事件。这里,我们的代码很明显不是主循环。我们的程序变成了事件循环的附属品,使得我们的程序成为了一组无需任何显式关联的、相互独立的事件处理程序的集合。


再举一个更加具体的例子,假设有一个与 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 操作时挂起),进入事件循环。


函数 getlineputline 模拟了同步 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 以内,上述代码与之前的同步示例等价。在同步代码结构的外表之下,程序其实是以事件驱动模式运行的。同时,该程序与以更典型的事件驱动风格编写的程序的其他部分也完全兼容。

目录
相关文章
|
6月前
|
设计模式 算法 Java
关于编程模式的总结与思考(上)
关于编程模式的总结与思考(上)
80 0
|
C语言
实现一个简单的事件驱动处理框架
实现一个简单的事件驱动处理框架
137 0
|
30天前
|
消息中间件 监控 测试技术
事件驱动架构是一种编程范式
【10月更文挑战第7天】事件驱动架构是一种编程范式
106 65
|
30天前
|
存储 设计模式 监控
事件驱动架构的实现方式?
【10月更文挑战第7天】事件驱动架构的实现方式?
36 7
|
5月前
|
存储 数据处理 数据库
理解在服务架构中的事件驱动
【6月更文挑战第14天】网络架构和软件设计常基于ISO七层模型和三层应用架构,强调数据处理的重要性。事件驱动架构(EDA)以事件为中心,改变传统设计方式,解决系统问题。事件是触发通知或状态变化的操作,如用户下单。EDA适用于微服务通信、工作流程自动化、SaaS集成和基础设施自动化等场景,提高系统敏捷性和可扩展性。然而,EDA并非万能,需根据需求选择合适的设计方案。
171 1
理解在服务架构中的事件驱动
|
6月前
|
存储 NoSQL Java
关于编程模式的总结与思考(中)
关于编程模式的总结与思考(中)
43 1
|
6月前
|
存储 监控 NoSQL
关于编程模式的总结与思考(下)
关于编程模式的总结与思考(下)
56 0
|
6月前
|
缓存
2.1.2事件驱动reactor的原理与实现
2.1.2事件驱动reactor的原理与实现
|
存储 索引
2.2 事件驱动的reactor网络设计模型
2.2 事件驱动的reactor网络设计模型
48 0
|
消息中间件 Linux
事件驱动的I/O模型是什么意思?底层原理是什么?
事件驱动的I/O模型是什么意思?底层原理是什么?
195 0
下一篇
无影云桌面