delphi 在数字运算应用程序中从后台线程向主线程发送消息

cxfofazt  于 2023-10-18  发布在  其他
关注(0)|答案(1)|浏览(119)

我在 Delphi 中有一个数字运算应用程序,它使用一个或多个后台线程来处理数字数据。这些后台线程同时运行多个并行进程。从这些线程中,我使用WM_COPYDATA消息将信息传递给主线程,以便向用户提供反馈。
在我的PC中,我有Intel Core i7- 10750 H CPU处理器(6个物理核心和12个线程),我的RAM是32 GB。当我同时运行一个或两个分析时(CPU使用率50- 80%,内存消耗1-2GB),主线程正确地传递和处理消息,即我在主窗体的GUI中获得反馈。然而,当我将系统推向极限时(在具有6个物理内核的PC中进行6次并行分析,CPU使用率> 90%,内存消耗> 2.5GB),通常情况下,消息不会传递到主线程,并且我在GUI中没有得到反馈(不知何故,它们似乎丢失了)。但是,后台线程可以正确运行,并且在分析结束时提供正确的结果。
是什么问题,消息没有被处理?我可以做些什么来解决这个问题吗?非常感谢。
数据通过以下记录传递:

type
  TCommunicationMsgRecord = record
    AnalysisID: integer;
    UpdateType: integer;
    MessageStr: String[255];
    prbProgress: integer;
    valX,valY: real;
  end;

在后台线程中的代码如下:

procedure TExecution.SetMainFormMemoProcess(const str: string);
var
  CommunicationMsgRecord: TCommunicationMsgRecord;
  copyDataStruct : TCopyDataStruct;
begin
      CommunicationMsgRecord.AnalysisID := Self.ExecutionID;
      CommunicationMsgRecord.UpdateType := 0;
      CommunicationMsgRecord.MessageStr := StrLeft(str,255);
      CommunicationMsgRecord.prbProgress := 0;
      CommunicationMsgRecord.valX := 0;
      CommunicationMsgRecord.valY := 0;
      //------------------------------------------------------
      copyDataStruct.dwData := 0;
      copyDataStruct.cbData := SizeOf(CommunicationMsgRecord);
      copyDataStruct.lpData := @CommunicationMsgRecord;
      SendMessage(MainForm.handle, WM_COPYDATA, Integer(hInstance), Integer(@copyDataStruct));
end;

主线程中的代码如下:

type
  TMainForm = class(TForm)
  private
    procedure WMCopyData( var Msg : TWMCopyData ); message WM_COPYDATA;

implementation
procedure TMainForm.WMCopyData(var Msg: TWMCopyData);
var
  CommunicationMsgRecord : TCommunicationMsgRecord;
begin
    try
        CommunicationMsgRecord:= TCommunicationMsgRecord(Msg.CopyDataStruct.lpData^);
        case CommunicationMsgRecord.UpdateType of
           0: MainForm.memoProcess.Lines.Add(CommunicationMsgRecord.MessageStr);
           1: MainForm.Statusbar.Panels[0].Text := CommunicationMsgRecord.MessageStr;
           //other options (e.g. plotting) that receive the data from CommunicationMsgRecord
           //....
        end;
    except
          on E: EAccessViolation do
           begin
              ShowMessage('Access Violation');
           end;
           on E: Exception do //this will catch all your other exceptions
           begin
              ShowMessage('Other Error');
           end;
      end;
end;
k5hmc34c

k5hmc34c1#

首先,在这种情况下使用WM_COPYDATA是多余的。它的目的是跨进程边界序列化数据,而您并没有这样做。因为你是在同一个进程中向一个窗口发送数据,所以只需要在自定义消息中直接发送数据指针。
另外,不要使用Integer()类型转换,因为它只能在32位中工作。如果你决定编译你的应用程序为64位(即,访问更多的内存),显示的代码 * 将 * 失败。SendMessage()的第四个参数是LPARAM,而不是IntegerLPARAM是32位或64位整数,具体取决于CPU架构。使用正确的类型。

type
  PCommunicationMsgRecord = ^TCommunicationMsgRecord;
  TCommunicationMsgRecord = record
    AnalysisID: integer;
    UpdateType: integer;
    MessageStr: String[255];
    prbProgress: integer;
    valX,valY: real;
  end;

const
  WM_MY_DATA_MSG = WM_APP + 100;

procedure TExecution.SetMainFormMemoProcess(const str: string);
var
  CommunicationMsgRecord: TCommunicationMsgRecord;
begin
  CommunicationMsgRecord.AnalysisID := Self.ExecutionID;
  CommunicationMsgRecord.UpdateType := 0;
  CommunicationMsgRecord.MessageStr := StrLeft(str,255);
  CommunicationMsgRecord.prbProgress := 0;
  CommunicationMsgRecord.valX := 0;
  CommunicationMsgRecord.valY := 0;

  SendMessage(MainForm.Handle, WM_MY_DATA_MSG, 0, LPARAM(@CommunicationMsgRecord));
end;

...

type
  TMainForm = class(TForm)
  private
    procedure WMMyDataMsg( var Msg : TMessage ); message WM_MY_DATA_MSG;
    ...
  end;

procedure TMainForm.WMMyDataMsg(var Msg: TMessage);
var
  CommunicationMsgRecord : PCommunicationMsgRecord;
begin
  try
    CommunicationMsgRecord := PCommunicationMsgRecord(Msg.LParam);
    case CommunicationMsgRecord.UpdateType of
      0: MainForm.MemoProcess.Lines.Add(CommunicationMsgRecord.MessageStr);
      1: MainForm.StatusBar.Panels[0].Text := CommunicationMsgRecord.MessageStr;
      //....
    end;
  except
    ...
  end;
end;

第二,使用MainForm.Handle的方式不是线程安全的。VCL * 可以 *(有时确实)在运行时动态地重新创建它的HWND。如果您碰巧在VCL重新创建HWND属性的同时在工作线程中读取Handle属性,那么可能会发生非常糟糕的事情。您应该使用一个单独的HWND,它保证是持久的,比如TApplication.Handle(使用TApplication.HookMainWindow()接收它的消息),或者更好的AllocateHWnd()
现在,如果你向主线程发送了很多消息,你可能会填满主线程的消息队列。队列一次只能容纳一定数量的消息(IIRC,限制是10000),但是您所显示的代码没有对SendMessage()调用执行任何错误检查,例如:。

procedure TExecution.SetMainFormMemoProcess(const str: string);
var
  ...
  ErrCode: DWORD;
begin
  ...
  if SendMessage(...) = 0 then
  begin
    ErrCode := GetLastError();
    if ErrCode = ERROR_NOT_ENOUGH_QUOTA then
      // the message queue is full ...
  end;
end;

另外,请注意SendMessage()是同步的,它不会返回,直到消息处理程序处理消息,这意味着你的工作线程将相互同步,减慢他们的处理,因为一个线程将不得不等待另一个线程,如果他们试图同时发送他们的数据。为了避免这种情况,请使用PostMessage()(并动态分配正在发布的数据),以便您的线程可以全速运行,例如:

procedure TExecution.SetMainFormMemoProcess(const str: string);
var
  CommunicationMsgRecord: PCommunicationMsgRecord;
begin
  New(CommunicationMsgRecord);
  CommunicationMsgRecord.AnalysisID := Self.ExecutionID;
  CommunicationMsgRecord.UpdateType := 0;
  CommunicationMsgRecord.MessageStr := StrLeft(str,255);
  CommunicationMsgRecord.prbProgress := 0;
  CommunicationMsgRecord.valX := 0;
  CommunicationMsgRecord.valY := 0;

  if not PostMessage(MainForm.Handle, WM_MY_DATA_MSG, 0, LPARAM(CommunicationMsgRecord)) then
  begin
    Dispose(CommunicationMsgRecord);
    ...
  end;
end;

...

procedure TMainForm.WMMyDataMsg(var Msg: TMessage);
var
  CommunicationMsgRecord : PCommunicationMsgRecord;
begin
  try
    CommunicationMsgRecord := PCommunicationMsgRecord(Msg.LParam);
    try
      ...
    finally
      Dispose(CommunicationMsgRecord);
    end;
  except
    ...
  end;
end;

但是,要小心,因为这可能会更快地填满主线程的消息队列,所以你应该考虑将数据结果存储在线程安全内存中的其他地方,然后让主线程定期显示最新的结果(例如,在计时器中),而不是在每次数据更新时显示。

相关问题