从线程发送字符串数据到主窗体

10

在 Delphi 中,我创建了一个线程,就像这样,它会不时地向主窗体发送消息。

Procedure TMyThread.SendLog(I: Integer);
Var
  Log: array[0..255] of Char;
Begin
  strcopy(@Log,PChar('Log: current stag is ' + IntToStr(I)));
   PostMessage(Form1.Handle,WM_UPDATEDATA,Integer(PChar(@Log)),0);
End;

procedure TMyThread.Execute;
var
  I: Integer;
begin
  for I := 0 to 1024 * 65536 do
  begin
    if (I mod 65536) == 0 then
    begin
      SendLog(I);
    End;
  End;
end;

其中WM_UPDATEDATA是一个自定义消息,如下所定义:

const
  WM_UPDATEDATA = WM_USER + 100;

在主表单中,它将按以下方式更新列表:

procedure TForm1.WMUpdateData(var msg : TMessage);
begin
  List1.Items.Add(PChar(msg.WParam));
end;

然而,由于发送到主窗体的日志字符串是一个局部变量,在调用SendLog后将被销毁。由于TForm1.WMUpdateData异步处理消息,因此在调用时,日志字符串可能已经被销毁。如何解决这个问题?

我认为可以在全局系统空间中分配字符串空间,然后将其传递给消息,然后在TForm1.WMUpdateData处理消息后可以销毁全局空间中的字符串空间。这是可行的解决方案吗?如何实现?

谢谢


1
请看这里:http://stackoverflow.com/questions/9932164/postmessage-lparam-truncation 希望能帮到你。 - Arkady
你需要将Log变量声明为全局变量。 - S.MAHDI
@S.MAHDI 不行!如果有两个排队的消息怎么办? - David Heffernan
考虑把这些字符串放在线程安全的列表中,并向主线程发送一条消息,让其知道你已经添加了一个。 - Marcus Adams
3个回答

12

除了您正在发布本地变量的事实外,TWinControl.Handle属性也不是线程安全的。您应该改用TApplication.Handle属性,或使用AllocateHWnd()创建自己的窗口。

您确实需要在堆上动态分配字符串,将该指针发布到主线程,然后在使用完毕后释放内存。

例如:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Application.OnMessage := AppMessage;
  // or use a TApplicationEvents component...
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  Application.OnMessage := nil;
end;

procedure TForm1.AppMessage(var Msg: TMsg; var Handled: Boolean);
var
  S: PString;
begin
  if Msg.Message = WM_UPDATEDATA then
  begin
    S := PString(msg.LParam);
    try
      List1.Items.Add(S^);
    finally
      Dispose(S);
    end;
    Handled := True;
  end;
end;

procedure TMyThread.SendLog(I: Integer);
var
  Log: PString;
begin
  New(Log);
  Log^ := 'Log: current stag is ' + IntToStr(I);
  if not PostMessage(Application.Handle, WM_UPDATEDATA, 0, LPARAM(Log)) then
    Dispose(Log);
end;

或者:

var
  hLogWnd: HWND = 0;

procedure TForm1.FormCreate(Sender: TObject);
begin
  hLogWnd := AllocateHWnd(LogWndProc);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  if hLogWnd <> 0 then
    DeallocateHWnd(hLogWnd);
end;

procedure TForm1.LogWndProc(var Message: TMessage);
var
  S: PString;
begin
  if Message.Msg = WM_UPDATEDATA then
  begin
    S := PString(msg.LParam);
    try
      List1.Items.Add(S^);
    finally
      Dispose(S);
    end;
  end else
    Message.Result := DefWindowProc(hLogWnd, Message.Msg, Message.WParam, Message.LParam);
end;

procedure TMyThread.SendLog(I: Integer);
var
  Log: PString;
begin
  New(Log);
  Log^ := 'Log: current stag is ' + IntToStr(I);
  if not PostMessage(hLogWnd, WM_UPDATEDATA, 0, LPARAM(Log)) then
    Dispose(Log);
end;

3
根据postmessage分配和释放数据并不是我建议的做法。你的消息可能永远不会被处理,从而创建难以检测的内存泄漏。在WINAPI中没有一次出现这种模式可能会表明这不是一个好主意。例如,消息队列大小受到限制。或者目标代码可能会更改并引入内存泄漏。或者目标窗口可能通过默认窗口过程处理WM_UPDATEDATA,因此内存永远不会被释放。等等... - JensG
@JensG 别担心。这是同一个应用程序的主要表单。应用程序的作者控制WM_UPDATEDATA的处理。验证内存是否释放并不难。而你担心主线程队列满了会导致内存泄漏?那只是微不足道的问题。我的主线程消息队列也满了,但天哪,我竟然泄漏了一些内存!!! - David Heffernan
@David Heffernan:一个解决方案明显存在多个缺点,而且有更好、更健壮的解决方案,但却被如此欢呼,这超出了我的理解范围。任何曾经追踪过某些服务器应用程序中奇怪的内存泄漏问题,而该应用程序预计可以连续运行数天或数月,但最终由于突然的OOM而死亡的人都会同意我的评论。在这种事情上,你永远不能太小心。显然你没有经历过那种情况,否则你不会那样说话。但当涉及到错误搜索时,这对你来说当然很棒。 - JensG
1
@JensG 有些人无法正确地做到这一点并不意味着它是不可能完成的。Remy 在这里展示的是准确的,也没有泄漏。 - David Heffernan
5
如果队列已满,就不会发生泄漏。PostMessage()将失败,然后原始线程会释放它分配的内存,因为它无法被发送。 - Remy Lebeau
显示剩余3条评论

12
如果您拥有D2009或更高版本,则可以使用另一种方法将消息发布到主窗体。TThread.Queue是来自线程的异步调用,其中可以在主线程中执行方法或过程。
优点在于设置消息传递的框架较简单。只需在创建线程时传递回调方法即可。无需处理句柄或显式处理字符串分配/释放。
Type
  TMyCallback = procedure(const s : String) of object;

  TMyThread = class(TThread)
    private
      FCallback : TMyCallback;
      procedure Execute; override;
      procedure SendLog(I: Integer);
    public
      constructor Create(aCallback : TMyCallback);
  end;

constructor TMyThread.Create(aCallback: TMyCallback);
begin
  inherited Create(false);
  FCallback := aCallback;
end;

procedure TMyThread.SendLog(I: Integer);
begin
  if not Assigned(FCallback) then
    Exit;
  Self.Queue(  // Executed later in the main thread
    procedure
    begin
      FCallback( 'Log: current stag is ' + IntToStr(I));
    end
  );
end;

procedure TMyThread.Execute;
var
  I: Integer;
begin
  for I := 0 to 1024 * 65536 do
  begin
    if ((I mod 65536) = 0) then
    begin
      SendLog(I);
    End;
  End;
end;

procedure TMyForm.TheCallback(const msg : String);
begin
  // Show msg
end;

procedure TMyForm.StartBackgroundTask(Sender : TObject);
begin
  ... 
  FMyThread := TMyThread.Create(TheCallback);
  ...
end;

0
使用 SendMessage()。
PostMessage() 将异步处理您的消息,它基本上将其放入目标消息队列中并立即返回。在处理程序代码访问以 wparam/lparam 发送的数据时,您的调用方已经释放了字符串。
相比之下,SendMessage() 绕过消息队列并直接调用窗口过程(同步)。当 SendMessage() 返回时,可以安全地释放字符串。

我可以补充一下,这不是唯一的解决方案,也可能不适用于所有情况或您特定的情况。在某些情况下,使用Synchronize()可能是另一个要考虑的选项。这基本上取决于您想要(或应该)将两个线程耦合在一起的严格程度。最简单的方法是使用一个引用计数的辅助对象仅用于数据交换。引用计数确保任何一个线程都可以首先释放它而不影响另一个线程。当然,辅助对象需要锁定或其他机制来控制并发。还有更多可能的解决方案... - JensG
人们选择使用PostMessage是因为他们想要异步传递消息。你忽略了这一点,建议使用同步方式。这将会对性能产生影响。因为现在工作线程必须等待主线程准备好来分派消息。 - David Heffernan
这就是为什么我添加了评论,指出这可能不是唯一和/或最佳的解决方案。你肯定已经读过了,对吧?而且你的论点只是推测性的。此外,OP并没有写明他选择PostMessage的意图。 - JensG
1
请在您的回答中将其带入讨论。解释同步与异步的优缺点。 - David Heffernan

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