如何可靠地等待刚刚创建的线程?

时间:2013-02-23 15:15:47

标签: delphi delphi-xe3

考虑以下计划:

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中的错误?有什么方法可以解决这个问题吗?我希望如果我让我的线程发出信号表明它已经启动(使用事件),那么这将解决问题。但这让我觉得很脏。

4 个答案:

答案 0 :(得分:14)

您可以将其称为错误或TThread设计漏洞,该问题已多次讨论过。请参阅示例http://sergworks.wordpress.com/2011/06/25/sleep-sort-and-tthread-corner-case/

问题是,如果TThread.Terminated标志设置得太早,则永远不会调用TThread.Execute方法。因此,在您的情况下,请不要在TThread.Terminate之前致电TThread.WaitFor

答案 1 :(得分:5)

我认为这种情况发生的原因已经被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线程。那一个很简单明了。它只是对队列的引用,以及一个运行直到队列为空的执行方法。

  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;

答案 2 :(得分:1)

我不认为这种行为是TThread中的错误。应该独立于当前线程的执行/异步执行新线程的执行。如果设置了这样的事情,以便在TThread.Create()将控制权返回给当前线程中的调用者之前保证新线程开始执行,那将意味着新线程的执行(部分)与当前线程同步。

在分配线程资源之后,新线程被添加到线程调度队列中。如果你从头开始构建一个新线程(我好像记得TThread那样),这可能需要一段时间,因为必须在幕后分配很多东西。避免获取线程的成本是创建ThreadPool.QueueUserWorkItem的原因。

此外,您所看到的行为与您所制定的说明非常吻合。构建一个新的TThread。立即终止它。为什么有任何期望新线程有机会执行?

如果您必须在创建线程时具有同步行为,则至少需要放弃当前线程上的剩余时间片。睡眠(0)就足够了。 Sleep(0)放弃当前时间片的其余部分,并立即返回到任何其他线程(以相同优先级)等待的后面的调度队列。

如果您发现Sleep(0)不足以在当前线程调用Terminate之前启动并运行新线程,那么线程创建开销可能会阻止新线程很快进入线程就绪队列满足你不耐烦的当前线程。在这种情况下,尝试通过构造处于挂起状态的新线程来分离线程构造的开销与执行,然后启动新线程,然后在当前线程中Sleep(0),然后终止新线程。这将为新线程提供在当前线程终止之前进入当前线程之前的线程就绪调度队列的最佳机会。

如果没有明确的合作或来自新线程内部的信号,这将在WinAPI中获得“定向产量”。来自新线程的显式合作/信令是保证调用线程将等待新线程开始执行之后的唯一方法。

线程之间的信令状态不脏。什么是脏的是期望/需要新的线程构造来阻止调用线程。

答案 3 :(得分:-1)

如前所述,您必须等待线程,直到它开始,然后再调用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