Delphi - 跨线程事件处理

3
我有一个小的客户端服务器应用程序,其中服务器使用命名管道向客户端发送一些消息。客户端有两个线程-主GUI线程和一个“接收线程”,它不断通过命名管道接收服务器发送的消息。现在每当接收到某个消息时,我想触发一个自定义事件-但是,该事件应该在主GUI线程上处理,而不是在调用线程上处理-我不知道如何做到这一点(以及是否可能)。
以下是我目前所拥有的:
tMyMessage = record
    mode: byte;
    //...some other fields...
end;

TMsgRcvdEvent = procedure(Sender: TObject; Msg: tMyMessage) of object;

TReceivingThread = class(TThread)
private
  FOnMsgRcvd: TMsgRcvdEvent;
  //...some other members, not important here...
protected
  procedure MsgRcvd(Msg: tMyMessage); dynamic;
  procedure Execute; override;
public
  property OnMsgRcvd: TMsgRcvdEvent read FOnMsgRcvd write FOnMsgRcvd;
  //...some other methods, not important here...
end;

procedure TReceivingThread.MsgRcvd(Msg: tMyMessage);
begin
  if Assigned(FOnMsgRcvd) then FOnMsgRcvd(self, Msg);
end;

procedure TReceivingThread.Execute;
var Msg: tMyMessage
begin
  //.....
  while not Terminated do begin //main thread loop
    //.....
    if (msgReceived) then begin
      //message was received and now is contained in Msg variable
      //fire OnMsgRcvdEvent and pass it the received message as parameter
      MsgRcvd(Msg); 
    end;
    //.....
  end; //end main thread loop
  //.....
end;

现在我想能够创建TForm1类的事件处理程序成员,例如:
procedure TForm1.MessageReceived(Sender: TObject; Msg: tMyMessage);
begin
  //some code
end;

这段文字的大意是:
“这个事件不会在接收线程中执行,而是在主 UI 线程中执行。我特别希望接收线程只触发事件并继续执行,而不需要等待事件处理程序方法的返回(基本上我需要像 .NET Control.BeginInvoke 方法一样的东西)。
我真的是个初学者(几个小时前我还试图学习如何定义自定义事件),所以我不知道它是否可能或者我是否做错了什么,所以非常感谢您的帮助。”
5个回答

2
您已经得到了一些答案,但没有一个提到您问题中令人困扰的部分:
tMyMessage = record
    mode: byte;
    //...some other fields...
end;

请注意,当您使用Delphi或其他用于本地Windows消息处理的包装器时,您可能会认为在.NET环境中可以做所有事情,但实际上不能。您可能期望能够将随机数据结构传递给事件处理程序,但这是行不通的。原因是需要进行内存管理。
在.NET中,您可以确信不再从任何地方引用的数据结构将通过垃圾回收处理掉。在Delphi中,您没有同样的自由度,您需要确保任何分配的内存块也正确释放。
在Windows中,消息接收者要么是窗口句柄(HWND),您可以将其SendMessage()或PostMessage()到,要么是线程,您可以将其PostThreadMessage()到。在两种情况下,消息只能携带两个数据成员,它们都是机器字宽度,第一个类型为WPARAM,第二个类型为LPARAM。你不能简单地将任何随机记录发送或发布为消息参数。
Delphi使用的所有消息记录类型基本具有相同的结构,这映射到上述数据大小限制。
如果您想将数据发送到由多个32位大小的变量组成的另一个线程,则事情变得棘手。由于可以发送的值的大小限制,您可能无法发送整个记录,而只能发送其地址。为此,您将在发送线程中动态分配数据结构,将地址作为消息参数之一传递,并在接收线程中重新解释相同的参数为具有相同类型的变量的地址,然后使用记录中的数据并释放动态分配的内存结构。
因此,根据您需要发送到事件处理程序的数据量,您可能需要更改tMyMessage记录。这可以工作,但比必要的困难,因为无法对事件数据进行类型检查。
我建议以略微不同的方式解决这个问题。您知道需要将哪些数据从工作线程传递到GUI线程。只需创建一个队列数据结构,将事件参数数据放入其中,而不是直接发送它们与消息。使此队列线程安全,即用关键部分保护它,以便在尝试同时从不同线程添加或删除时仍然安全。
要请求新的事件处理,请简单地将数据添加到您的队列中。当先前为空的队列添加第一个数据元素时,才向接收线程发布消息。然后,接收线程应接收和处理消息,并继续从队列中弹出数据元素并调用匹配的事件处理程序,直到队列再次为空。为了获得最佳性能,队列应尽可能短暂地锁定,并且在调用事件处理程序时绝对应该暂时取消锁定。

2

你应该使用PostMessage(异步)或SendMessage(同步)API向窗口发送消息。你也可以使用某种形式的“队列”,或者使用非常棒的OmniThreadLibrary来完成这个任务(强烈推荐)。


1
请勿使用同步锁,+1。最好提供一个使用post/send消息的示例,特别是因为原帖作者明确表示自己是一个绝对的初学者... - Marjan Venema
1
同步操作非常适合初学者,因为它不会让初学者深入了解Windows消息传递的细节。 - Eugene Mayevski 'Callback

1

声明一个私有成员

FRecievedMessage: TMyMEssage

还有一个受保护的过程

procedure PostRecievedMessage;
begin
   if Assigned(FOnMsgRcvd) then FOnMsgRcvd(self, FRecievedMessage);
   FRecievedMessage := nil;
end;

并将循环中的代码更改为

if (msgReceived) then begin
  //message was received and now is contained in Msg variable
  //fire OnMsgRcvdEvent and pass it the received message as parameter
  FRecievedMessage := Msg;
  Synchronize(PostRecievedMessage); 
end;

如果你想完全异步地执行它,请使用 PostMessage API。


0

0

请查看Synchronize方法的文档。它是为像您这样的任务而设计的。


1
唉。同步会暂停所有次要线程并在主线程的上下文中执行,这意味着它基本上击败了使用多个线程的目的。有比同步更好的方法。不过我不会给你投反对票,因为尽管这是一个糟糕的答案,但从技术上讲它是有效的。 :-) - Ken White
2
谁告诉你了这个神话?同步并不会阻塞“所有”的辅助线程。它只会阻塞调用它的线程,这是由于它的同步性质所致。但其他线程仍然在运行。 - Eugene Mayevski 'Callback
只有调用线程被阻塞。这不是最好的机制,但如果你知道它的工作原理,应该没问题。 - Runner
1
使用 Synchronize() 意味着确保在调用返回时事件处理程序已经完成,即忽略问题的以下部分:“我特别希望接收线程只触发事件并在执行中继续而不等待事件处理程序方法的返回”。 - mghie
它会阻塞任何想要调用“同步”方法的其他线程。 - David Heffernan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接