【问题标题】:Send string data from Thread to main form将字符串数据从 Thread 发送到主窗体
【发布时间】:2013-10-04 07:09:06
【问题描述】:

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

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;

但是,由于发送到主窗体的Log字符串是一个局部变量,调用SendLog后会被销毁。而 TForm1.WMUpdateData 是异步处理消息的,所以有可能在调用它的时候,Log 字符串已经被销毁了。如何解决这个问题?

我想也许我可以在全局系统空间中分配字符串空间,然后将其传递给消息,然后TForm1.WMUpdateData处理消息后,它可以破坏全局空间中的字符串空间。这是一个可行的解决方案吗?如何实现?

谢谢

【问题讨论】:

  • 看这里stackoverflow.com/questions/9932164/… 希望对你有帮助
  • 您需要将您的 Log 变量声明为全局变量。
  • @S.MAHDI 不!如果有两条排队的消息怎么办?
  • 考虑将这些字符串放在线程安全列表中,并将消息发回主线程,让其知道您添加了一个。

标签: multithreading delphi message postmessage


【解决方案1】:

如果您有 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;

【讨论】:

    【解决方案2】:

    除了您发布一个局部变量这一事实之外,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;
    

    【讨论】:

    • 我不推荐基于 postmessage 分配数据和解除分配。您的消息可能永远不会被处理,然后您会创建一个难以检测的内存泄漏。在 WINAPI 中没有出现这种模式可能会表明,这不是一个好主意。例如,消息队列的大小是有限的。或者目标代码可能会改变并引入内存泄漏。或者目标窗口可能通过默认窗口过程处理 WM_UPDATEDATA,因此内存永远不会被释放。等等……
    • @JensG 别担心。这是同一应用程序的主要形式。该应用程序的作者控制着WM_UPDATEDATA 的处理。验证内存是否被释放并不难。当你的主线程队列满时你担心内存泄漏?!这是你最不担心的事情。我的主线程消息队列已满,但是,天哪,我泄露了一些内存!!!!
    • @David Heffernan:尽管存在更好、更强大的解决方案,但有几个明显缺点的解决方案如何受到欢呼,这超出了我的理解。任何曾经在某些服务器应用程序中追踪到奇怪的内存泄漏的人都将同意我的评论。你只是对这些事情不够小心。你显然没有经历过,否则你不会那样说。当然,这对您来说非常有用,因此您可以在错误搜索方面大放异彩。
    • @JensG 有些人无法做到这一点的事实并不意味着它不能做到。 Remy 在这里展示的内容是准确的,不会泄露。
    • 如果队列满了就没有泄漏。 PostMessage() 将失败,然后原线程释放它分配的内存,因为它无法发布。
    【解决方案3】:

    使用 SendMessage()。

    PostMessage() 将异步处理您的消息,它基本上放入目标消息队列并立即返回。在处理程序代码访问 wparam/lparam 中发送的数据时,您的调用者已经释放了字符串。

    相比之下,SendMessage() 绕过消息队列并直接(同步)调用窗口 proc。在 SendMessage() 返回时,释放字符串是安全的。

    【讨论】:

    • 我可以补充一下,这不是唯一的解决方案,它可能并不适用于所有情况。你的具体情况。在某些情况下,使用 Synchronize() 可能是另一个需要考虑的选项。它基本上取决于您希望(或应该)将两个线程耦合在一起的严格程度。最失败的方法是仅用于数据交换的引用辅助对象。 refcount 确保任何一个线程都可以先释放它而不影响另一个线程。当然,辅助对象需要一个锁或其他一些机制来控制并发。还有更多可能的解决方案...
    • 人们选择 PostMessage 是因为他们想要异步传递。您忽略这一点并建议同步。这将对性能产生影响。因为现在工作线程必须等待主线程准备好发送消息。
    • 这就是为什么我添加了它可能不是唯一和/或最佳解决方案的评论。你肯定读过这篇文章,不是吗?最重要的是,您的论点只是推测性的。 OP 没有写下他为什么选择 PostMessage 的意图。
    • 所以在你的回答中把它带入你的讨论中。解释同步与异步的优缺点。
    猜你喜欢
    • 2010-10-16
    • 1970-01-01
    • 2013-03-31
    • 2016-09-02
    • 2013-03-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多