響應式 GUI 使用執行緒進行後臺工作,PostMessage 使用執行緒進行報告

在執行冗長的程序時保持 GUI 響應需要一些非常精細的回撥來允許 GUI 處理其訊息佇列,或者使用(後臺)(工作)執行緒。

開始任意數量的執行緒來完成一些工作通常不是問題。當你想要使 GUI 顯示中間和最終結果或報告進度時,開始有趣。

在 GUI 中顯示任何內容都需要與控制元件和/或訊息佇列/泵進行互動。這應該始終在主執行緒的上下文中完成。從不在任何其他執行緒的上下文中。

有很多方法可以解決這個問題。

此示例顯示瞭如何使用簡單執行緒執行此操作,允許 GUI 在完成後通過將 FreeOnTerminate 設定為 false 來訪問執行緒例項,並使用 PostMessage 報告執行緒何時完成

有關競爭條件的註釋:對工作執行緒的引用儲存在表單中的陣列中。執行緒完成後,陣列中的相應引用將被取消。

這是競爭條件的潛在來源。正如使用 Running 布林值一樣,可以更容易地確定是否還有任何需要完成的執行緒。

你需要決定是否需要使用鎖來保護這些資源。

在這個例子中,沒有必要。它們僅在兩個位置進行修改:StartThreads 方法和 HandleThreadResults 方法。這兩種方法只能在主執行緒的上下文中執行。只要你保持這種方式並且不從不同執行緒的上下文開始呼叫這些方法,就沒有辦法讓它們產生競爭條件。

type
  TWorker = class(TThread)
  private
    FFactor: Double;
    FResult: Double;
    FReportTo: THandle;
  protected
    procedure Execute; override;
  public
    constructor Create(const aFactor: Double; const aReportTo: THandle);

    property Factor: Double read FFactor;
    property Result: Double read FResult;
  end;

建構函式只設定私有成員並將 FreeOnTerminate 設定為 False。這是必不可少的,因為它將允許主執行緒查詢執行緒例項的結果。

execute 方法執行計算,然後將訊息釋出到它在建構函式中收到的控制代碼,說明它已完成:

procedure TWorker.Execute;
const
  Max = 100000000;var
  i : Integer;
begin
  inherited;

  FResult := FFactor;
  for i := 1 to Max do
    FResult := Sqrt(FResult);

  PostMessage(FReportTo, UM_WORKERDONE, Self.Handle, 0);
end;

在這個例子中,使用 PostMessage 是必不可少的。PostMessage``just 將訊息放入主執行緒訊息泵的佇列中,並且不等待它被處理。它本質上是非同步的。如果你要使用 SendMessage,你就會把自己編成泡菜。SendMessage 將訊息放入佇列並等待,直到它被處理完畢。簡而言之,它是同步的。

自定義 UM_WORKERDONE 訊息的宣告宣告為:

const
  UM_WORKERDONE = WM_APP + 1;
type
  TUMWorkerDone = packed record
    Msg: Cardinal;
    ThreadHandle: Integer;
    unused: Integer;
    Result: LRESULT;
  end;

UM_WORKERDONE const 使用 WM_APP 作為其值的起點,以確保它不會干擾 Windows 或 Delphi VCL 使用的任何值(如 MicroSoft 推薦的那樣)。

形成

任何形式都可用於啟動執行緒。你需要做的就是新增以下成員:

private
  FRunning: Boolean;
  FThreads: array of record
    Instance: TThread;
    Handle: THandle;
  end;
  procedure StartThreads(const aNumber: Integer);
  procedure HandleThreadResult(var Message: TUMWorkerDone); message UM_WORKERDONE;

哦,示例程式碼假定在表單的宣告中存在 Memo1: TMemo;,它用於記錄和報告

FRunning 可用於防止 GUI 在工作進行時被點選。FThreads 用於儲存例項指標和建立的執行緒的控制代碼。

啟動執行緒的過程有一個非常簡單的實現。它首先檢查是否已經有一組執行緒在等待。如果是這樣,它就會退出。如果沒有,它將標誌設定為 true 並啟動執行緒,為每個執行緒提供自己的控制代碼,以便他們知道在哪裡釋出他們的完成訊息。

procedure TForm1.StartThreads(const aNumber: Integer);
var
  i: Integer;
begin
  if FRunning then
    Exit;
    
  FRunning := True;

  Memo1.Lines.Add(Format('Starting %d worker threads', [aNumber]));
  SetLength(FThreads, aNumber);
  for i := 0 to aNumber - 1 do
  begin
    FThreads[i].Instance := TWorker.Create(pi * (i+1), Self.Handle);
    FThreads[i].Handle := FThreads[i].Instance.Handle;
  end;
end;

執行緒的控制代碼也放在陣列中,因為這是我們在訊息中收到的訊息,它告訴我們執行緒已經完成並且線上程的例項外部使它更容易訪問。如果我們不需要例項來獲取結果(例如,如果它們已儲存在資料庫中),那麼線上程例項外部使用控制代碼也允許我們使用 FreeOnTerminate 設定為 True。在這種情況下,當然不需要保留對例項的引用。

樂趣在於 HandleThreadResult 實現:

procedure TForm1.HandleThreadResult(var Message: TUMWorkerDone);
var
  i: Integer;
  ThreadIdx: Integer;
  Thread: TWorker;
  Done: Boolean;
begin
  // Find thread in array
  ThreadIdx := -1;
  for i := Low(FThreads) to High(FThreads) do
    if FThreads[i].Handle = Cardinal(Message.ThreadHandle) then
    begin
      ThreadIdx := i;
      Break;
    end;

  // Report results and free the thread, nilling its pointer and handle 
  // so we can detect when all threads are done.
  if ThreadIdx > -1 then
  begin
    Thread := TWorker(FThreads[i].Instance);
    Memo1.Lines.Add(Format('Thread %d returned %f', [ThreadIdx, Thread.Result]));
    FreeAndNil(FThreads[i].Instance);
    FThreads[i].Handle := nil;
  end;

  // See whether all threads have finished.
  Done := True;
  for i := Low(FThreads) to High(FThreads) do
    if Assigned(FThreads[i].Instance) then
    begin
      Done := False;
      Break;
    end;
  if Done then
  begin
    Memo1.Lines.Add('Work done');
    FRunning := False;
  end;
end;

此方法首先使用訊息中收到的控制代碼查詢執行緒。如果找到匹配,它會使用例項檢索並報告執行緒的結果(FreeOnTerminateFalse,記得嗎?),然後結束:釋放例項並將例項引用和控制代碼都設定為 nil,表明此執行緒為 no 更長的相關。

最後,它檢查是否有任何執行緒仍在執行。如果沒有找到,則報告全部完成並將 FRunning 標誌設定為 False,以便可以開始新的一批工作。