从Thread发布消息到GUI最佳实践?

ALZ*_*ALZ 3 delphi messaging multithreading delphi-7

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

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

  • 使用Synchronize从每一个工作者线程:
  • 使用共享缓冲区和Windows消息传递机制.线程将消息放入共享缓冲区(队列),并将通过Windows消息通知GUI.
  • 使用单独的线程,它将侦听同步原语(事件,信号量等)并再次使用Synchronize,但仅限于GUI专用线程,或GUI上的Critical Section来显示消息.
  • 更新:(由一位同事提出)使用共享缓冲区和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;
Run Code Online (Sandbox Code Playgroud)

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

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

消息处理程序

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;
Run Code Online (Sandbox Code Playgroud)

有什么坏事吗?主窗体在应用程序启动时分配ONCE,在应用程序退出时销毁.

Dis*_*ned 6

使用来自每个工作线程的Synchronize

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

但是应该注意,还有其他原因可以避免阻塞.阻塞可以使性能分析变得有点棘手,因为它有效地增加了"赶紧等待"的例程所花费的时间.

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

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

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

但是,由于您提到了共享缓冲区,因此您可能会获得更多数据.这是你必须要小心的地方.直观地使用"共享缓冲区"可以让您了解竞争条件(但我将在后面详细介绍).

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

  • 创建一个新对象,其中包含GUI更新所需的所有详细信息.
  • 通过其他参数传递对该对象的引用PostMessage.
  • 当GUI完成处理消息时,它负责销毁它.
  • 这巧妙地避免了竞争条件.
  • 警告:您需要确定GUI获取所有消息,否则您将发生内存泄漏.您必须检查返回值PostMessage以确认它实际已发送,如果没有发送,您也可以销毁该对象.
  • 如果可以在轻量级对象中发送数据,则此方法可以很好地工作.

使用单独的线程......

使用任何类型的单独的中间线程仍然需要类似的考虑因素来获取相关数据到新线程 - 然后仍然必须以某种方式传递给GUI.如果您的应用程序需要在更新GUI之前执行聚合和耗时的计算,这可能才有意义.与您不想阻止IO线程的方式相同,您不希望阻止GUI线程.

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

我前面提到了共享缓冲区的"直观思想",意思是:"不同的线程同时读写"; 让您面临竞争风险.如果在写入操作的中间开始读取数据,则可能会以不一致的状态读取数据.这些问题可能是调试的噩梦.

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

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

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

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