10

考虑以下程序:

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.

输出是:

线程
完成的

TThreadWithSleep
1
2
3
4
5
完成的

因此,出于某种原因,主线程似乎必须等待任意时间才能终止并等待工作线程。我是否认为这是一个错误TThread?有什么办法可以解决这个问题吗?我希望如果我让我的线程表明它已经开始(使用事件),那么这将解决这个问题。但这让我觉得很脏。

4

4 回答 4

15

你可以称之为错误或TThread设计缺陷,这个问题已经讨论过很多次了。参见例如http://sergworks.wordpress.com/2011/06/25/sleep-sort-and-tthread-corner-case/

问题是如果TThread.Terminated标志设置得太早,TThread.Execute则永远不会调用方法。所以在你的情况下,不要打电话TThread.Terminatebefore TThread.WaitFor

于 2013-02-23T15:22:27.967 回答
5

我认为发生这种情况的原因已经被 Serg 的回答充分回答,但我认为你通常不应该调用 Thread.Terminate 无论如何。如果您希望线程终止,例如在应用程序关闭时,调用它的唯一原因。如果您只想等到它完成,您可以调用 WaitFor(或 WaitForSingleObject)。这是可能的,因为线程的句柄已经在其构造函数中创建,因此您可以立即调用它。

此外,我在这些线程上将 FreeOnTerminate 设置为 true。就让他们跑吧,让他们自由自在。如果我想要通知他们,我可以使用 WaitFor 或 OnTerminate 事件。

这只是一堆工作线程以阻塞方式清空队列的示例。

我认为你不应该需要这个,大卫,但也许其他人可能会对一个例子感到满意。另一方面,您可能不会问这个问题只是为了对 TThread 的糟糕实现进行更改,对吧?;-)

首先是队列类。我认为这不是一个真正的传统队列。在真正的多线程队列中,您应该能够随时添加到队列中,即使处理处于活动状态。此队列要求您预先填充其项目,然后调用 -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;

然后是消费者线程。那是简单易行的。它只有一个对队列的引用,以及一个一直运行到队列为空的执行方法。

  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;

计时器仅用于更新进度条。它获取正在运行的线程数(只有在处理几乎完成时才会减少),并获取“项目”,这实际上是下一个要处理的项目。因此,实际上最后 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;
于 2013-02-25T19:32:10.377 回答
2

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

线程资源分配完毕后,新线程加入线程调度队列。如果您从头开始构建一个新线程(我似乎记得 TThread 确实如此),这可能需要一段时间,因为必须在幕后分配很多东西。避免启动线程的成本是创建 ThreadPool.QueueUserWorkItem 的原因。

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

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

如果您观察到 Sleep(0) 不足以在当前线程调用 Terminate 之前启动并运行新线程,那么线程创建开销可能会阻止新线程尽快进入线程就绪队列以满足您的不耐烦当前线程。在这种情况下,尝试通过在挂起状态下构造新线程,然后启动新线程,然后在当前线程中休眠(0),然后终止新线程,来分离线程构造和执行的开销。这将使新线程在当前线程终止之前有最好的机会在当前线程之前进入线程就绪调度队列。

这与您将在 WinAPI 中获得“定向收益”一样接近,而无需来自新线程内部的显式合作或信号。来自新线程的显式合作/信号是保证调用线程将等到新线程开始执行之后的唯一方法。

线程之间的信号状态不是脏的。肮脏的是期望/需要新的线程构造来阻塞调用线程。

于 2013-03-01T22:04:17.520 回答
-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
完成的
于 2014-09-17T12:26:39.807 回答