3

我有一个必须完全运行、不中断并返回结果的函数。如果异步事件导致在它仍在执行时再次调用它,则必须以某种方式阻止该调用,直到第一次调用完成。

互斥体?还有什么?


[更新] 该函数在我的主窗体类中,从该类的两个方法调用:一个处理从 serail 端口读取的数据,另一个处理计时器到期。这两个似乎都在它们自己的线程中运行,因为一个函数的调用可以被另一个调用中断(对我来说,在应用程序级别,它们只是我在设计时放在我的主窗体上的组件)。

也许TCriticalSection?(但谷歌搜索不清楚我是否需​​要使用acquire/release或“进入/离开”,而且似乎由于代码是可能主窗体的简单函数,它将是可重入的)。

也许我要求的是一种使代码“不可重入”的方法,阻塞直到第一个条目完成?不管答案是什么,我认为我需要一个代码示例,或者一个 UTl :-(

(请注意,此页面提供了许多有用的信息(我并没有完全理解,但对其他人来说看起来非常有用))


[更新] 这与 GUI 更新无关,只是每个发送一些 TCOP 数据的事件都应该收到响应。

虽然该应用程序有一个 GUI - 它是基于表单的 - 只是在我想显示一些调试信息的情况下,因为该应用程序将在没有显示器的 PC 上运行(是的,我知道它仍然有一个 GUI,但是不是我的问题/观点)

4

4 回答 4

7

将函数移动到没有异步事件的单独工作线程。

主线程中的异步事件应该将一些消息发布到某个队列中,因此当工作线程完成执行时,主线程将使用新参数再次启动它。


有更多信息的更新。假设(猜测)这些函数可以通过 RS232 接收到的数据或什么都没有,并且正在执行类似于 GUI 更新的操作,我将概述以下方法。

  1. 使用窗体的 Windows 消息队列进行警报。
  2. 使用一些互锁的队列对象进行数据传递。(是的,我知道我可以将指针放入 Windows 消息中,但我想要更多的类型安全性)
  3. 使用外工作螺纹进行长加工。

更多细节:

  1. grep WM_USER 的 VCL 源代码并查看模式。您声明了 3 个消息 ID 常量:Work_Complete、Work_Request、Work_EmptyQueue。以及相应的消息方法形式。
  2. 我会使用来自 OmniThreadLibrary 的就绪线程安全队列。但是您可以继承 System.Contnrs.TQueue 或 System.Generics.Collections.TQueue,将它们所有的数据传递方法封装到临界区。总的来说,我建议你看看 OmniThreadLibrary 管道,如果它们适合你的话
  3. 这是硬件数据工作应用程序的标准方式。无论是 MS-DOS 设备驱动程序。或者一些嵌入式设备。您应该将快速和精益的数据保存与长期工作区分开来。

因此,RS232 事件处理程序将从 COM 读取数据,将其放入 TBytes,然后将该数据包添加到输入 TQueue。更复杂一点的方法是查看 Queue 是否已经包含来自 COM 的数据并将新数据包与旧数据包聚合,而不是将新数据包分开。这需要更仔细的锁定,所以在这里聚合可能不值得

计时器甚至会制作空包(零长度字节数组)并将其排入队列。如果该计时器甚至还有一些数据要传递-那么它应该是变体记录或单独的输入队列或其他任何东西。但是没有信息,似乎计时器只发送任何信息,但警报本身

根据您的说法,Timer 和 RS232 事件都在各自的线程中工作。我对此表示怀疑,但我必须相信你。

在将工作负载排入队列后(例如在输入队列的通知事件中),我会执行 win32 PostMessage(MainForm.Handle,work_request)。毕竟我们希望将线程控制集中在一个地方。为了保持线程隔离,应该发布消息,没有 SendMessage 没有 TControl.Perform!

在表单的 work_request 处理程序的主线程中,我会查看输入队列是否已经不是空的。如果没有,那么我会查看工作线程状态。如果它被暂停,我会恢复它。

工作线程看起来是输入队列有东西,虽然它有,但它会:

  1. 从队列中提取数据包到本地变量
  2. 做一些可能很长的处理
  3. 制作输出“工作成果”包。假设是 GUI 更新,它可能只是键值对的集合,或者一些数据字段的记录,其中包含哪些数据字段已填充,哪些数据字段将被忽略。
  4. 将该数据包排入输出队列(排队然后将 Work_Complete msg 发布到主窗体)
  5. 循环到 1。

如果输入队列为空,则函数退出,线程执行 PostMessage Work_EmptyQueue 并暂停自身,直到稍后被唤醒以完成更多工作或被释放。

当 MainForm (再次在主线程中)收到 Work_Complete 时

  1. 将输出队列中的所有数据包提取到本地变量中
  2. 合并它们。(如果我有 p1=(label1='aaaa', label2='bbbb') 和 p2=(label3='cccc', label1='dddd') 那么累积的任务将被设置(label1='dddd', label2 ='bbbb', label3='cccc');
  3. 应用它们(实际上是更新 GUI)

如果输出队列为空,则省略第 2 步和第 3 步(如果在上一步发生合并)。但不是第四。第 1 步和第 2 步是分开的。由于队列操作是线程互锁的,因此步骤 1 的目标是尽可能快地提取数据。保证金将在当地完成。

当 MainForm 收到 Work_EmptyQueue 时,它​​会检查输入队列现在是否不为空,并且可能会恢复工作线程。它可以选择对状态或其他任何东西进行一些 GUI 更新。

那是一个粗略的草图。它可以更好地雕刻给你一定的引用。

于 2012-08-15T13:48:22.883 回答
7

假设您担心来自同一个线程的重入,那么阻塞重入调用将导致典型的死锁。如果这是您的情况,那么您需要执行以下操作之一:

  1. 确保不会发生重入调用,或者
  2. 检测重入呼叫并通过将其添加到队列以供以后处理来推迟它,或者
  3. 检测重入呼叫并忽略它。

很可能这些选项都不吸引您!

如果您担心来自不同线程的同时调用,那么您可以使用某种锁。例如,在 Windows 上,您通常会使用临界区。

于 2012-08-15T13:49:00.837 回答
5

结合使用两种机制:

  1. 使用 TCriticalSection 防止另一个线程进入。显然这只是多线程应用程序所必需的。
  2. 使用再入门。这是一个简单的布尔值,用于防止从同一线程重新进入。如果您尝试保护的功能已经输入,则不要阻止,而是反弹。“反弹”意味着采取一些适当的行动,例如将所需的活动排队以供以后执行。
于 2012-08-15T13:48:38.897 回答
1

另一种方法是当从一个事件源(例如,串行端口处理程序)进入关键函数时,在调用期间禁用另一个事件源(到期计时器)。通话后重新启用它。由于计时器已被禁用,您可以保证在通话期间其他事件源无法触发。

对于要处理串行数据包时关闭到期计时器的情况,这很简单。如果我们假设到期计时器用于跟踪一段时间内没有从串行端口接收到任何内容,则在接收到的串行数据时禁用到期计时器是合适的。处理完串行数据包后恢复/重启到期定时器。

相反的情况——在处理到期定时器时接收串行数据——可能有点棘手,因为您可能不想通过禁用串行数据接收器来冒丢失数据的风险。如果您的串行数据处理程序可以轻松暂停/恢复而不会丢失数据,那么您就完成了!否则,您最好的选择可能是在对象实例中使用布尔标志来指示关键函数何时执行。

如果一个串行数据包进来并且该标志为真,则将串行数据包推入一个列表以供稍后处理并立即返回。设置您的到期计时器处理程序以在从关键调用返回之前和之后立即检查该列表。如果列表非空,则处理列表中的所有项目。如果在您进入到期计时器处理程序时列表中有等待处理的数据包,您可能希望忽略到期计时器到期并继续处理。

在串行数据处理程序中,在调用关键函数之前和之后检查列表是否非空。之前检查将有助于确保串行数据包按照接收顺序进行处理。检查after将确保在处理当前数据包过程中到达的后续串行数据不会丢失。

如果有可能在不同的线程上调用串行数据事件处理程序,则应以线程安全的方式小心处理对布尔标志和列表的更新。如果串行事件处理程序(和到期计时器处理程序)保证在 UI 线程上执行,那么您不必担心这一点。

于 2012-08-16T17:37:57.483 回答