【问题标题】:Post message from Thread to GUI best practice?将消息从 Thread 发布到 GUI 最佳实践?
【发布时间】:2014-01-16 20:02:34
【问题描述】:

我正在开发小型监控应用程序,该应用程序将有一些线程通过 SNMP、TCP、ICMP 与某些设备进行通信,其他线程必须执行一些计算。 我必须在 GUI 中输出所有这些结果(一些表单或 TabSheets)。

我正在考虑下一个可能性:

  • 在每个工作线程中使用Synchronize
  • 使用共享缓冲区和 Windows 消息传递机制。线程会将消息放入共享缓冲区(队列)中,并通过 Windows 消息通知 GUI。
  • 使用单独的线程来侦听同步原语(事件、信号量等)并再次使用Synchronize,但只能从 GUI 专用线程或 GUI 上的关键部分显示消息。
  • 更新:(由一位同事提出)使用共享缓冲区和 TTimer 主要形式,它将定期(100-1000 毫秒)检查共享缓冲区和消耗,而不是 Windows 消息传递。 (与消息传递相比,它有什么好处吗?)
  • 其他?

尊敬的专家,请解释最佳做法是什么,或者公开替代方案的优缺点是什么。

更新:
作为想法:
//共享缓冲区 + 发送消息变体
LogEvent 全局函数将从任何地方(也从工作线程)调用:

procedure LogEvent(S: String);
var
  liEvent: IEventMsg;
begin
  liEvent := TEventMsg.Create; //Interfaced object
  with liEvent do
  begin
    Severity := llDebug;
    EventType := 'General';
    Source := 'Application';
    Description := S;
  end;
  MainForm.AddEvent(liEvent); //Invoke main form directly
end;

在主窗体中,事件列表视图和共享部分(fEventList: TTInterfaceList 已经是线程安全的)我们将是:

procedure TMainForm.AddEvent(aEvt: IEventMsg);
begin
  fEventList.Add(aEvt);
  PostMessage(Self.Handle, WM_EVENT_ADDED, 0, 0);
end;

消息处理程序:

procedure WMEventAdded(var Message: TMessage); message WM_EVENT_ADDED;
...
procedure TMainForm.WMEventAdded(var Message: TMessage);
var
  liEvt: IEventMsg;
  ListItem: TListItem;
begin
  fEventList.Lock;
  try
    while fEventList.Count > 0 do
    begin
      liEvt := IEventMsg(fEventList.First);
      fEventList.Delete(0);
      with lvEvents do //TListView
      begin
        ListItem := Items.Add;
        ListItem.Caption := SeverityNames[liEvt.Severity];
        ListItem.SubItems.Add(DateTimeToStr(now));
        ListItem.SubItems.Add(liEvt.EventType);
        ListItem.SubItems.Add(liEvt.Source);
        ListItem.SubItems.Add(liEvt.Description);
      end;
    end;
  finally
    fEventList.UnLock;
  end;
end;

有什么不好的吗?主窗体在应用程序启动时分配一次,在应用程序退出时销毁。

【问题讨论】:

  • 它们看起来都差不多。哪个更好?谁知道。我们无法真正分辨,因为我们不知道所有的约束。性能重要吗?是什么推动了您的设计?
  • @TLama:当然消息可能会丢失,消息队列的长度是有限的,因此发布消息的速度快于处理速度会导致队列已满,消息“丢失”除非发送者正确处理PostMessage()的错误结果。见stackoverflow.com/questions/123323/…
  • 不需要定时器;你最好使用 Application.OnIdle 来检查缓冲区。
  • 顺便说一句。如果您打算在 WMEventAdded 方法中使用整个队列(您似乎这样做了),请不要忘记锁定该列表视图的更新(Items.BeginUpdateItems.EndUpdate)。这将锁定绘制控件,因此列表视图不会在系统要求时每次添加项目时自行重新绘制。
  • @ALZ “曾几何时,我编写了一个 Delphi HelloWorld 应用程序...” 这就是为什么您应该始终检查 PostMessage() 的返回值的原因。此外,填满消息队列的最简单方法是在您发布到的同一线程的循环内调用PostMessage()。因为在您发布时没有任何东西会删除队列条目。因此,一般来说,您要警惕向自己发布大量消息的线程。

标签: multithreading delphi delphi-7 messaging


【解决方案1】:

在每个工作线程中使用同步

这可能是最简单的实现方法,但正如其他人所指出的那样,会导致您的 IO 线程被阻塞。这在您的特定应用程序中可能/可能不是问题。

但是应该注意,还有其他原因可以避免阻塞。阻塞可以使性能分析变得有点棘手,因为它有效地增加了在“匆忙等待”的例程中花费的时间。

使用共享缓冲区和 Windows 消息传递机制

这是一个很好的方法,有一些特殊的考虑。

如果您的数据非常小,PostMessage 可以将其全部打包到消息的参数中,使其成为理想选择。

但是,由于您提到了共享缓冲区,因此您似乎可能拥有更多数据。这是你必须要小心的地方。使用直观意义上的“共享缓冲区”可能会使您面临竞争条件(但我稍后会更详细地介绍)。

更好的方法是创建一个消息对象并将对象的所有权传递给 GUI。

  • 创建一个新对象,其中包含更新 GUI 所需的所有详细信息。
  • 通过PostMessage 中的附加参数传递对该对象的引用。
  • 当 GUI 处理完消息后,它负责销毁它。
  • 这巧妙地避免了竞争条件。
  • 警告:您需要确保 GUI 能够获取您的所有消息,否则您将出现内存泄漏。你必须检查PostMessage的返回值来确认它确实被发送了,如果没有发送你也可以销毁这个对象。
  • 如果数据可以以轻量级对象的形式发送,则此方法非常有效。

使用单独的线程...

使用任何类型的单独中间线程仍然需要类似的考虑,以便将相关数据传输到新线程 - 然后 仍然 必须以某种方式传递给 GUI。如果您的应用程序需要在更新 GUI 之前执行聚合和耗时的计算,这可能才有意义。就像你不想阻塞你的 IO 线程一样,你也不想阻塞你的 GUI 线程。

在主窗体中使用共享缓冲区和TTimer

前面提到过共享缓冲区的“直观想法”,意思是:“不同线程同时读写”;使您面临竞争条件的风险。如果在写操作的中间开始读取数据,那么您可能会在不一致的状态下读取数据。这些问题可能是调试的噩梦。

为了避免这些竞争条件,您需要依靠其他同步工具(例如锁)来保护共享数据。锁当然会让我们回到阻塞问题,尽管形式稍微好一点。这是因为您可以控制所需保护的粒度。

这确实比消息传递有一些好处:

  • 如果您的数据结构庞大而复杂,您的消息可能效率低下。
  • 您无需定义严格的消息传递协议来涵盖所有更新场景。
  • 消息传递方法可能会导致系统内的数据重复,因为 GUI 会保留自己的数据副本以避免竞争条件。

有一种方法可以改进共享数据的概念,仅在适用的情况下:在某些情况下,您可以选择使用 不可变 数据结构。即:数据结构在创建后不会更改。 (注意:前面提到的消息对象应该是不可变的。)这样做的好处是您可以安全地读取数据(从任意数量的线程)而无需任何同步原语 - 只要您可以保证数据不会更改。

【讨论】:

  • 通过 PostMessage 中的附加参数传递对该对象的引用是意外内存泄漏的一扇大门,恕我直言 - 恕我直言,线程安全队列更安全:在这种情况下,您只需发送通知消息,并且从队列中检索所有未决信息,如果需要,甚至绕过不推荐使用的值。如果访问不需要很多时间,锁定就不是一个大问题 - 而不是 不可变 结构,您可以只使用私有副本或取消排队信息以进行进一步处理,然后释放锁定。
【解决方案2】:

最好的方法是使用 GDI 自定义消息并调用 PostMessage() 来通知 GUI。

type
  TMyForm = class(TForm)
  .
  .
  .
  private
    procedure OnMyMessage(var Msg: TMessage); message WM_MY_MESSAGE;
    procedure OnAnoMessage(var Msg: TMessage); message WM_ANO_MESSAGE;
  .
  .


  PostMessage(self.Handle,WM_MY_MESSAGE,0,0);

this great article for full explanation

这是一种依赖操作系统内部功能的更轻/更快的方法。

【讨论】:

  • 请注意TWinControl.Handle 属性getter 不是线程安全的!您应该 1) 发布到 TApplication.Handle 并使用 TApplication(Events).OnMessage 事件来检索它,或者 2) 发布到由 AllocateHWnd() 创建的专用窗口。
  • 你怎么能确定这是最好的?
  • @DavidHeffernan OP 征求意见。这只是我的,来自多年使用这种方法的反馈。正如我所写,恕我直言,这是最好的方法,因为它“是一种依赖操作系统内部功能的更轻/更快的方法”,而任何其他 TThread/Synchronize 方法都是一个阻塞过程,容易出错。跨度>
  • @ArnaudBouchez 问题是窗口娱乐。 AllocateHWnd 是正确的方法。
  • @ALZ 您需要一个不受重新创建的窗口句柄。 AllocateHWnd 就是你要找的。​​span>
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-23
  • 2011-07-11
  • 1970-01-01
  • 2011-03-07
相关资源
最近更新 更多