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

时间:2013-12-29 22:54:54

标签: multithreading delphi delphi-7 messaging

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

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

  • 使用每个工作线程中的Synchronize
  • 使用共享缓冲区和Windows消息传递机制。线程将消息放入共享缓冲区(队列),并将通过Windows消息通知GUI。
  • 使用单独的线程来监听同步原语(事件,信号量等)并再次使用Synchronize,但只能从GUI专用线程或GUI上的Critical Section显示消息。
  • 更新:(由一位同事提议)使用主窗体中的共享缓冲区和TTimer,它将定期检查(100-1000 ms)共享缓冲区并消耗,而不是使用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;

在主窗体中,事件ListView和共享部分(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;

有什么不好的吗?主表单在应用程序启动时分配ONCE,在应用程序退出时销毁。

2 个答案:

答案 0 :(得分:5)

使用每个工作线程的同步

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

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

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

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

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

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

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

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

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

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

以主窗体

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

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

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

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

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

答案 1 :(得分:1)

最好的方法是使用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

这是一种更轻/更快的依赖OS内部功能的方法。