【问题标题】:How to design a state machine in face of non-blocking I/O?面对非阻塞I/O,如何设计状态机?
【发布时间】:2009-08-12 10:18:50
【问题描述】:

我正在使用默认情况下具有非阻塞 I/O 的 Qt 框架来开发一个应用程序,该应用程序可以在多个网页(在线商店)中导航并在这些页面上执行不同的操作。我正在将特定网页“映射”到用于浏览此页面的状态机。
这个状态机有这些转换;
Connect, LogIn, Query, LogOut, Disconnect
以及这些状态;
Start, Connecting, Connected, LoggingIn, LoggedIn, Querying, QueryDone, LoggingOut, LoggedOut, Disconnecting, Disconnected
从 *ing 状态到 *ed 状态 (Connecting->Connected) 的转换是由于在加载当前请求的 url 时从网络对象接收到的 LoadFinished 异步网络事件。从 *ed 到 *ing 状态 (Connected->LoggingIn) 的转换是由于我发送的事件。
我希望能够向这台机器发送多个事件(命令)(如 Connect、LogIn、Query("productA")、Query("productB")、LogOut、LogIn、Query("productC")、LogOut、Disconnect) 立即并让它处理它们。我不想阻止等待机器完成处理我发送给它的所有事件。问题是它们必须与上述网络事件交错,通知机器正在下载的 url。没有交错,机器无法推进其状态(并处理我的事件),因为从 *ing 推进到 *ed 仅在接收到网络类型的事件后发生。

如何实现我的设计目标?

编辑

  1. 我正在使用的状态机有自己的事件循环,并且事件没有在其中排队,因此如果机器忙时它们来了,机器可能会错过它们。
  2. 网络 I/O 事件既不会直接发布到状态机,也不会直接发布到我正在使用的事件队列中。它们被发布到我的代码(处理程序)中,我必须处理它们。我可以随心所欲地转发它们,但请记住没有。 1.
  3. 看看我对这个问题的答案,我在其中详细描述了我当前的设计。问题是我是否以及如何通过制作它来改进这个设计

    • 更健壮
    • 更简单

【问题讨论】:

    标签: c++ qt events state-machine qstatemachine


    【解决方案1】:

    听起来您希望状态机有一个事件队列。将事件排队,开始处理第一个事件,完成后将下一个事件从队列中拉出并开始处理。所以状态机不是由客户端代码直接驱动,而是由队列驱动。

    这意味着任何涉及在下一个转换中使用一个转换的结果的逻辑都必须在机器中。例如,如果“登录完成”页面告诉您下一步该去哪里。如果这不可能,那么事件可能包含机器可以调用的回调,以返回它需要知道的任何内容。

    【讨论】:

    • 不错的建议,但它并没有解决问题。如果我将我的事件机器排队等待网络事件...
    • 哦,除非您的意思是网络事件会卡在客户端代码发出的事件后面的队列中。在这种情况下,您需要 另一个 队列 - 一个网络对象用于异步 I/O 完成的队列,另一个用于记住客户端告诉状态机做什么的队列。
    • @onebyone 你能否澄清第三个队列的目的是什么?
    • 我建议使用两个队列——你已经拥有的一个,异步 I/O 系统正在使用它来传递与网络通信相关的事件,另外还有一个队列,可能完全由状态机管理.如果状态机被指示执行一个还不可能的转换,那么它将一个项目添加到这个队列中。每当它完成转换时,它都会检查队列以查看是否还有更多工作可以做。
    • @onebyone 异步 I/O 事件被传递到我的代码,而不是直接传递到队列或机器。我必须手动转发它们。除了机器自己的事件循环不排队它接收到的事件。所以可能会出现这样的情况,当网络事件进入时机器很忙,它会错过这个事件。我不希望这种情况发生。现在我只有一个队列,完全由我管理。我自己也管理 I/O 事件。请参阅我对这个问题的回答,其中我详细描述了我当前的设计。
    【解决方案2】:

    问这个问题,我已经有了一个工作设计,我不想写关于不要向任何方向歪曲答案:) 我将在这个伪答案中描述我的设计是什么。

    除了状态机之外,我还有一个事件队列。我没有将事件直接发布到机器上,而是将它们放入队列中。然而,异步且随时出现的网络事件存在问题。如果队列不为空并且出现网络事件,则我无法将其放入队列中,因为在处理队列中已经存在的事件之前机器将等待它。并且机器将永远等待,因为此网络事件在之前放入队列中的所有事件之后等待。
    为了克服这个问题,我有两种类型的消息;正常和优先的。正常的是我发的,优先的都是网络的。当我收到网络事件时,我不会将其放入队列中,而是将其直接发送到机器。这样,它可以在从事件队列中拉出下一个事件之前完成其当前任务并进入下一个状态。
    之所以这样设计,是因为我的事件和网络事件恰好是 1:1 交错的。因此,当机器等待网络事件时,它并不忙于做任何事情(因此它已准备好接受它并且不会错过它),反之亦然 - 当机器等待我的任务时,它只是在等待我的任务而不是另一个网络一。

    我问这个问题是希望得到比现在更简单的设计。

    【讨论】:

      【解决方案3】:

      严格来说,你不能。因为你只有状态“正在连接”,所以你不知道之后是否需要顶部登录。您必须引入一个状态“ConnectingWithIntentToLogin”来表示从 Start 状态开始的“Connect, then Login”事件的结果。

      “Connecting”和“ConnectingWithIntentToLogin”状态之间自然会有很多重叠。这最容易通过支持状态层次结构的状态机架构来实现。

      --- 编辑---

      阅读您后来的反应,现在很清楚您的实际问题是什么。

      显然,您确实需要额外的状态,无论是在 FSM 中根深蒂固的状态,还是在单独队列中的外部状态。让我们按照您喜欢的模型,在队列中添加额外的事件。这里的问题是您想知道如何将那些排队的事件与实时事件“交错”。您不会 - 进入特定状态时会主动提取队列中的事件。在您的情况下,这些将是“*ed”状态,例如“已连接”。只有当队列为空时,您才会保持“已连接”状态。

      【讨论】:

      • 这是很好的信息,但它不是我问题的答案。现在我总是在连接后登录,所以我只有Connecting 状态。假设我将添加ConnectingWithIntentToLogin 状态。怎么办?这个问题的优点是如何强制事件交错。
      • 简单:如果在 ConnectingWithIntentToLogin 状态下您收到“连接”事件,则不会转换为“已连接”。相反,您执行登录命令并进入“登录”状态。有什么可以“交错”?
      • @MSalters 我只有一种网络事件,即LoadFinished。你怎么知道作为这个事件的 inside 处理程序你应该生成什么样的事件?目前,当我收到此网络事件时,我总是将相同的Loaded 事件发送到机器,当已经处于Connecting 状态时,它会转换到Connected 状态,或者如果已经在@ 状态下转换到LoggedIn 状态987654328@ 州。
      • 哦,我错过了那个细节。但那时它几乎无关紧要。它只是意味着 LoadFinished 必须由可以接收它的所有状态处理。这些状态不会是您从队列中拉出事件的状态。
      • @MSalters 从你的 cmets 我看你很了解这个主题,所以我想听听你的更多 cmets。你描述的情况就是现在的情况。然而,我有一种强烈的感觉,在当前的设计中,我的应用程序的逻辑与网页导航的逻辑混为一谈。这是两个不同的东西,我想找到可以将它们分开的设计。我也很难决定数据应该放在哪里并传输 - 在事件中,在转换中,在插槽中还是在多个地方?这是一个有趣的问题,也许我应该在一个新问题中提出这个问题。想法?
      【解决方案4】:

      如果您不想阻止,则表示您不关心网络回复。另一方面,如果您对回复感兴趣,则必须阻止等待它们。否则尝试设计您的 FSM 将很快导致您的自动机的大小达到无穷大。

      【讨论】:

      • 在我发送一系列事件的那一刻,我并不关心网络回复,但这些事件中的每一个都关心,因为它需要来自先前事件的信息。因为这些事件是由我发送的,所以你可以说我关心网络回复 a) 传递性和 b) 未来:)
      【解决方案5】:

      如何将状态机移动到不同的线程,即。 e. Q线程。我会在状态机中实现一个输入队列,这样我就可以发送非阻塞查询和一个输出队列来读取查询结果。如果查询结果到达,您甚至可以通过 connect(...) 在主线程中回调一个开槽函数,Qt 在这方面是线程安全的。

      这样,您的状态机可以在需要时阻塞,而不会阻塞您的主程序。

      【讨论】:

        【解决方案6】:

        听起来你只是想在后台做一个阻塞 I/O 的列表。

        所以有一个线程执行:

        while( !commands.empty() )
        {
          command = command.pop_back();
          switch( command )
          {
          Connect: 
            DoBlockingConnect();
            break;
          ...
          }
        }
        NotifySenderDone();
        

        【讨论】:

        • 1.我不能在我使用的框架中阻塞 I/O。 2. 这不是关于执行一系列阻塞 I/O 操作,因为在每次 I/O 操作之后,我必须将控制权交给状态机,由它决定下一步做什么。
        • 1) 请注意,在 QT 网络模块中,“waitfor...”语句将非阻塞 IO 转换为阻塞 IO。例如:socket.connectToHost(serverName, serverPort); socket.waitForConnected(); 2) 意识到您给出的命令列表与阻塞 IO 操作具有简单的 1-1 对应关系。这似乎您的设计意图是保持 IO 状态机简单和愚蠢,让 IO 状态机的“客户端”决定(通过命令列表)I/O 序列。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-05-24
        • 1970-01-01
        • 1970-01-01
        • 2018-04-06
        • 2013-08-22
        • 2016-09-16
        • 2012-06-18
        相关资源
        最近更新 更多