簡體   English   中英

如何可靠地等待剛剛創建的線程?

[英]How do I reliably wait on a thread that has just been created?

考慮以下程序:

program TThreadBug;
{$APPTYPE CONSOLE}

uses
  SysUtils, Classes, Windows;

type
  TMyThread = class(TThread)
  protected
    procedure Execute; override;
  end;

procedure TMyThread.Execute;
var
  i: Integer;
begin
  for i := 1 to 5 do begin
    Writeln(i);
    Sleep(100);
  end;
end;

procedure UseTThread;
var
  Thread: TMyThread;
begin
  Writeln('TThread');
  Thread := TMyThread.Create;
  Thread.Terminate;
  Thread.WaitFor;
  Thread.Free;
  Writeln('Finished');
  Writeln;
end;

procedure UseTThreadWithSleep;
var
  Thread: TMyThread;
begin
  Writeln('TThreadWithSleep');
  Thread := TMyThread.Create;
  Sleep(100);
  Thread.Terminate;
  Thread.WaitFor;
  Thread.Free;
  Writeln('Finished');
  Writeln;
end;

begin
  UseTThread;
  UseTThreadWithSleep;
  Readln;
end.

輸出是:

TThread
Finished

TThreadWithSleep
1
2
3
4
5
Finished

所以看起來,由於某種原因,主線程必須等待一段任意的時間才能終止並等待工作線程。 我是否正確地認為這是TThread一個錯誤? 有什么方法可以解決這個問題嗎? 我希望如果我讓我的線程發出信號表明它已經啟動(使用事件),那么這將解決問題。 但這讓我覺得很臟。

您可以將其稱為錯誤或TThread設計缺陷,該問題已多次討論過。 例如參見http://sergworks.wordpress.com/2011/06/25/sleep-sort-and-tthread-corner-case/

問題是,如果過早設置TThread.Terminated標志,則永遠不會調用TThread.Execute方法。 所以在你的情況下,不要在TThread.Terminate之前調用TThread.WaitFor

我認為這種情況發生的原因已經被Serg的答案充分回答了,但我認為你通常不應該調用Thread.Terminate。 調用它的唯一原因,如果您希望線程終止,例如在應用程序關閉時。 如果您只想等到它完成,您可以調用WaitFor(或WaitForSingleObject)。 這是可能的,因為線程的句柄已經在其構造函數中創建,因此您可以立即調用它。

另外,我在這些線程上將FreeOnTerminate設置為true。 讓他們自己跑步和解脫。 如果我想要通知它們,我可以使用WaitFor或OnTerminate事件。

這里只是一堆工作線程以阻塞方式清空隊列的示例。

我認為你不應該需要這個,大衛,但也許別人可能會對一個例子感到滿意。 另一方面,你可能沒有問這個問題只是為了改變對TThread執行不力的咆哮,對吧? ;-)

首先是Queue類。 我想,這不是一個真正的傳統隊列。 在實際的多線程隊列中,您應該能夠在任何時候添加到隊列中,即使處理處於活動狀態也是如此。 此隊列要求您預先填充其項目,然后調用-blocking-run方法。 此外,處理的項目將保存回隊列。

type
  TQueue = class
  strict private
    FNextItem: Integer;
    FRunningThreads: Integer;
    FLock: TCriticalSection;
    FItems: TStrings; // Property...
  private

    // Signal from the thread that it is started or stopped.
    // Used just for indication, no real functionality depends on this.
    procedure ThreadStarted;
    procedure ThreadEnded;

    // Pull the next item from the queue.
    function Pull(out Item: Integer; out Value: string): Boolean;

    // Save the modified value back in the queue.
    procedure Save(Item: Integer; Value: string);

  public
    property Items: TStrings read FItems;
    constructor Create;
    destructor Destroy; override;

    // Process the queue. Blocking: Doesn't return until every item in the
    // queue is processed.
    procedure Run(ThreadCount: Integer);

    // Statistics for polling.
    property Item: Integer read FNextItem;
    property RunningThreads: Integer read FRunningThreads;
  end;

然后是Consumer線程。 那一個很簡單明了。 它只是對隊列的引用,以及在隊列為空之前運行的execute方法。

  TConsumer = class(TThread)
  strict private
    FQueue: TQueue;
  protected
    procedure Execute; override;
  public
    constructor Create(AQueue: TQueue);
  end;

在這里,您可以看到這個模糊的“隊列”的實現。 它的主要方法是Pull和Save,消費者使用它來提取下一個項目,然后保存處理后的值。

另一個重要的方法是Run,它啟動給定數量的工作線程並等待所有工作線程完成。 所以這實際上是一個阻塞方法,它只在隊列清空后返回。 我在這里使用WaitForMultipleObjects,它允許您在需要添加額外技巧之前等待多達64個線程。 它與您在問題中的代碼中使用WaitForSingleObject相同。

看看Thread.Terminate從未被調用過?

{ TQueue }

constructor TQueue.Create;
// Context: Main thread
begin
  FItems := TStringList.Create;
  FLock := TCriticalSection.Create;
end;

destructor TQueue.Destroy;
// Context: Main thread
begin
  FLock.Free;
  FItems.Free;
  inherited;
end;

function TQueue.Pull(out Item: Integer; out Value: string): Boolean;
// Context: Consumer thread
begin
  FLock.Acquire;
  try
    Result := FNextItem < FItems.Count;
    if Result then
    begin
      Item := FNextItem;
      Inc(FNextItem);
      Value := FItems[Item];
    end;
  finally
    FLock.Release;
  end;
end;

procedure TQueue.Save(Item: Integer; Value: string);
// Context: Consumer thread
begin
  FLock.Acquire;
  try
    FItems[Item] := Value;
  finally
    FLock.Release;
  end;
end;

procedure TQueue.Run(ThreadCount: Integer);
// Context: Calling thread (TQueueBackgroundThread, or can be main thread)
var
  i: Integer;
  Threads: TWOHandleArray;
begin
  if ThreadCount <= 0 then
    raise Exception.Create('You no make sense no');
  if ThreadCount > MAXIMUM_WAIT_OBJECTS then
    raise Exception.CreateFmt('Max number of threads: %d', [MAXIMUM_WAIT_OBJECTS]);

  for i := 0 to ThreadCount - 1 do
    Threads[i] := TConsumer.Create(Self).Handle;

  WaitForMultipleObjects(ThreadCount, @Threads, True, INFINITE);
end;

procedure TQueue.ThreadEnded;
begin
  InterlockedDecrement(FRunningThreads);
end;

procedure TQueue.ThreadStarted;
begin
  InterlockedIncrement(FRunningThreads);
end;

消費者線程的代碼簡單明了。 它標志着它的開始和結束,但這只是美化,因為我希望能夠顯示正在運行的線程的數量,一旦創建所有線程,它就是最大值,並且只在第一個線程退出后才開始下降(是,當正在處理隊列中的最后一批項目時)。

{ TConsumer }

constructor TConsumer.Create(AQueue: TQueue);
// Context: calling thread.
begin
  inherited Create(False);
  FQueue := AQueue;
  // A consumer thread frees itself when the queue is emptied.
  FreeOnTerminate := True;
end;

procedure TConsumer.Execute;
// Context: This consumer thread
var
  Item: Integer;
  Value: String;
begin
  inherited;

  // Signal the queue (optional).
  FQueue.ThreadStarted;

  // Work until queue is empty (Pull returns false).
  while FQueue.Pull(Item, Value) do
  begin
    // Processing can take from .5 upto 1 second.
    Value := ReverseString(Value);
    Sleep(Random(500) + 1000);

    // Just save modified value back in queue.
    FQueue.Save(Item, Value);
  end;

  // Signal the queue (optional).
  FQueue.ThreadEnded;
end;

當然,如果要查看進度(或至少一點),則不需要阻止Run方法。 或者,就像我一樣,您可以在單獨的線程中執行該阻塞方法:

  TQueueBackgroundThread = class(TThread)
  strict private
    FQueue: TQueue;
    FThreadCount: Integer;
  protected
    procedure Execute; override;
  public
    constructor Create(AQueue: TQueue; AThreadCount: Integer);
  end;

    { TQueueBackgroundThread }

constructor TQueueBackgroundThread.Create(AQueue: TQueue; AThreadCount: Integer);
begin
  inherited Create(False);
  FreeOnTerminate := True;
  FQueue := AQueue;
  FThreadCount := AThreadCount;
end;

procedure TQueueBackgroundThread.Execute;
// Context: This thread (TQueueBackgroundThread)
begin
  FQueue.Run(FThreadCount);
end;

現在,從GUI本身調用它。 我創建了一個表單,它包含兩個進度條,兩個備忘錄,一個計時器和一個按鈕。 Memo1充滿了隨機字符串。 處理完成后,Memo2將接收處理過的字符串。 計時器用於更新進度條,按鈕是實際執行操作的唯一選擇。

因此,表單只包含所有這些字段,以及對隊列的引用。 它還包含一個事件處理程序,以便在處理完成時得到通知:

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    Memo2: TMemo;
    Timer1: TTimer;
    ProgressBar1: TProgressBar;
    ProgressBar2: TProgressBar;
    procedure Button1Click(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    Q: TQueue;
    procedure DoAllThreadsDone(Sender: TObject);
  end;

Button1單擊事件,初始化GUI,創建包含100個項目的隊列,並啟動后台線程來處理隊列。 此后台線程接收OnTerminate事件處理程序(TThread的默認屬性),以在處理完成時向GUI發送信號。

你可以在主線程中調用Q.Run,​​但它會阻止你的GUI。 如果這是你想要的,那么你根本不需要這個線程!

procedure TForm1.Button1Click(Sender: TObject);
// Context: GUI thread
const
  ThreadCount = 10;
  StringCount = 100;
var
  i: Integer;
begin
  ProgressBar1.Max := ThreadCount;
  ProgressBar2.Max := StringCount;

  Memo1.Text := '';
  Memo2.Text := '';

  for i := 1 to StringCount do
    Memo1.Lines.Add(IntToHex(Random(MaxInt), 10));

  Q := TQueue.Create;
  Q.Items.Assign(Memo1.Lines);
  with TQueueBackgroundThread.Create(Q, ThreadCount) do
  begin
    OnTerminate := DoAllThreadsDone;
  end;
end;

處理線程完成時的事件處理程序。 如果您希望處理阻止GUI,那么您不需要此事件處理程序,只需將此代碼復制到Button1Click的末尾即可。

procedure TForm1.DoAllThreadsDone(Sender: TObject);
// Context: GUI thread
begin
  Memo2.Lines.Assign(Q.Items);
  FreeAndNil(Q);
  ProgressBar1.Position := 0;
  ProgressBar2.Position := 0;
end;

計時器僅用於更新進度條。 它獲取正在運行的線程數(只在處理幾乎完成時才會下降),並且它獲取“Item”,這實際上是要處理的下一個項目。 因此,當實際上最后10個項目仍在處理時,它可能已經完成。

procedure TForm1.Timer1Timer(Sender: TObject);
// Context: GUI thread
begin
  if Assigned(Q) then
  begin
    ProgressBar1.Position := Q.RunningThreads;
    ProgressBar2.Position := Q.Item;
    Caption := Format('%d, %d', [Q.RunningThreads, Q.Item]);
  end;
  Timer1.Interval := 20;
end;

我不認為這種行為是TThread中的一個錯誤。 應該獨立於當前線程的執行/異步執行新線程的執行。 如果設置了這樣的事情,以便在TThread.Create()將控制權返回給當前線程中的調用者之前保證新線程開始執行,那將意味着新線程的執行(部分)與當前線程同步。

在分配線程資源之后,新線程被添加到線程調度隊列中。 如果你從頭開始構建一個新線程(我好像記得TThread那樣),這可能需要一段時間,因為必須在幕后分配很多東西。 避免獲取線程的成本是創建ThreadPool.QueueUserWorkItem的原因。

此外,您所看到的行為非常符合您所制定的說明。 構建一個新的TThread。 立即終止它。 為什么有任何期望新線程有任何執行機會?

如果您必須在創建線程時具有同步行為,則至少需要放棄當前線程上的剩余時間片。 睡眠(0)就足夠了。 Sleep(0)放棄當前時間片的其余部分,並立即返回到任何其他線程(以相同優先級)等待的后面的調度隊列。

如果您發現Sleep(0)不足以在當前線程調用Terminate之前啟動並運行新線程,那么線程創建開銷可能會阻止新線程很快進入線程就緒隊列以滿足您的不耐煩當前線程。 在這種情況下,嘗試通過構造處於掛起狀態的新線程來分離線程構造的開銷與執行,然后啟動新線程,然后在當前線程中Sleep(0),然后終止新線程。 這將為新線程提供在當前線程終止之前進入當前線程之前的線程就緒調度隊列的最佳機會。

這與WinAPI中的“定向產量”非常接近,沒有明確的合作或來自新線程內部的信號。 來自新線程的顯式合作/信令是保證調用線程將等待新線程開始執行之后的唯一方法。

線程之間的信令狀態不臟。 什么是臟的是期望/需要新的線程構造來阻止調用線程。

如前所述,在調用Terminate之前必須等待線程直到它開始,否則將永遠不會調用TThread.Execute 為此,您可以等到屬性TThread.Startedtrue

while not Thread.Started do;

你也可以在等待線程啟動時調用TThread.Yield ,因為這樣

通知系統它可以將執行傳遞給當前處理器上的下一個調度線程。 操作系統將選擇下一個線程。

while not Thread.Started do
  TThread.Yield;

至少我們最終會得到

procedure UseTThreadWithYield;
var
  Thread: TMyThread;
begin
  Writeln('TThreadWithYield');
  Thread := TMyThread.Create;

  // wait for the thread until started
  while not Thread.Started do
    TThread.Yield;

  Thread.Terminate;
  Thread.WaitFor;
  Thread.Free;
  Writeln('Finished');
  Writeln;
end;

和這樣生成的輸出

TThreadWithYield
1
2
3
4
5
Finished

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM