简体   繁体   中英

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

Consider the following program:

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.

The output is:

TThread
Finished

TThreadWithSleep
1
2
3
4
5
Finished

So it seems that, for some reason, the main thread has to wait some arbitrary amount of time before terminating and waiting for the worker thread. Am I right in thinking that this is a bug in TThread ? Is there any way I can work around this? I expect that if I get my thread to signal that it has started (using an event), then that would work around the issue. But that makes me feel dirty.

You can call it a bug or a TThread design flaw, the problem was discussed many times. See for example http://sergworks.wordpress.com/2011/06/25/sleep-sort-and-tthread-corner-case/

The problem is that if TThread.Terminated flag is set too early TThread.Execute method is never called. So in your case just don't call TThread.Terminate before TThread.WaitFor .

I think the reason why this happens, has been sufficiently answered by Serg's answer, but I think you should not normally call Thread.Terminate anyway. The only reason to call it, if you want the thread to terminate, for instance when the application is closing. If you just want to wait until it is finished, you can call WaitFor (or WaitForSingleObject). This is possible, because the handle for the thread is already created in its constructor, so you can call it right away.

Also, I set FreeOnTerminate to true on these threads. Just let them run and free themselves. If I want a notification of them to be done, I can use either WaitFor or the OnTerminate event.

Here's just an example of a bunch of worker threads emptying a queue in a blocking way.

I would think you shouldn't need this, David, but maybe someone else may be happy with an example. On the other hand, you probably didn't ask this question just to have a change to rant about TThread's poor implementation, right? ;-)

First the Queue class. It's not really a traditional queue, I think. In a real multi-threaded queue, you should be able to add to the queue at any point, even when the processing is active. This queue requires you to fill its items upfront, then call the -blocking- run method. Also, the processed items are saved back to the queue.

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;

Then the Consumer thread. That one is plain and easy. It just has a reference to the queue, and an execute method that runs until the queue is empty.

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

Here you see the implementation of this obscure 'Queue'. It's main methods are Pull and Save, which are used by the Consumer to pull the next item, and save the processed value back.

Another important method is Run, which starts a given number of worker threads and waits until all of them are finished. So this is actually a blocking method, which only returns after the queue is emptied. I'm using WaitForMultipleObjects here, which allows you to wait for upto 64 threads before you need to add extra tricks. It's the same as using WaitForSingleObject in the code in your question.

See how Thread.Terminate is never called?

{ 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;

The code for the consumer thread is plain and easy. It signals its start and end, but that's just cosmetic, because I want to be able to show the number of running threads, which is at it's max as soon as all threads are created, and only starts declining after the first thread exits (that is, when the last batch of items from the queue are being processed).

{ 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;

Of course, if you want to view the progress (or at least a little), you don't want a blocking Run method. Or, like I did, you can execute that blocking method in a separate thread:

  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;

Now, calling this from the GUI itself. I've created a form, that holds two progress bars, two memo's, a timer and a button. Memo1 is filled with random strings. Memo2 will receive the processed strings after processing is fully done. The timer is used to update the progress bars, and the button is the only thing that actually does something.

So, the form just contains all these fields, and a reference to the queue. It also contains an event handler to be notified when processing is complete:

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 click event, initializes the GUI, creates the queue with 100 items, and starts a background thread to process the queue. This background thread receives an OnTerminate event handler (default property for TThread) to signal the GUI when processing is done.

You can just call Q.Run in the main thread, but then it will block your GUI. If that is what you want, then you don't need this thread at all!

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;

The event handler for when the processing thread is done. If you want the processing to block the GUI, then you don't need this event handler and you can just copy this code to the end of Button1Click.

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

Timer is just for updating the progress bars. It fetches the number of running threads (which will only decline when processing is almost done), and it fetched the 'Item', which is actually the next item to process. So it may look finished already when actually the last 10 items are still being processed.

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;

I don't see this behavior as a bug in TThread. Execution of the new thread is supposed to occur independently of / asynchronous to the execution of the current thread. If things were set up such that the new thread was guaranteed to begin execution before TThread.Create() returns control to the caller in the current thread, that would mean execution of the new thread was (partially) synchronous to the current thread.

The new thread is added to the thread scheduling queue after the thread resources are allocated. If you're constructing a new thread from scratch (I seem to recall TThread does), this can take awhile because a lot of stuff has to be allocated behind the scenes. Avoiding this cost of getting a thread started is why ThreadPool.QueueUserWorkItem was created.

Furthermore, the behavior you're seeing fits perfectly well with the instructions you've laid out. Construct a new TThread. Immediately terminate it. Why is there any expectation that the new thread will have any opportunity to execute?

If you must have synchronous behavior around thread creation, you will at a minimum need to relinquish your remaining timeslice on the current thread. Sleep(0) will suffice. Sleep(0) gives up the rest of your current timeslice and immediately gets back into the scheduling queue behind whatever other threads (at your same priority) are waiting.

If you observe that Sleep(0) is not sufficient to getting the new thread up and running before the current thread calls Terminate, then thread creation overhead is probably preventing the new thread from getting into the thread-ready queue soon enough to satisfy your impatient current thread. In this case, try separating the overhead of thread construction from execution by constructing the new thread in the suspended state, then Start the new thread, then Sleep(0) in the current thread, then Terminate the new thread. This will give the new thread the best chance to get into the thread-ready schedule queue ahead of the current thread before the current thread terminates it.

This is as close as you're going to get to a "directed yield" in WinAPI without explicit cooperation or signaling from inside the new thread. Explicit cooperation / signalling from the new thread is the only way to guarantee that the calling thread will wait until after the new thread begins executing.

Signalling state between threads isn't dirty. What is dirty is expecting/requiring new thread construction to block the calling thread.

As already explained you must wait for the thread until it has started before calling Terminate otherwise TThread.Execute will never be called. To do so you can wait until the property TThread.Started is true .

while not Thread.Started do;

Also you can call TThread.Yield while waiting for the thread to start, because this

notifies the system that it can pass the execution to the next scheduled thread on the current processor. The operating system will select the next thread.

while not Thread.Started do
  TThread.Yield;

At least we will end up with

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;

and a generated output like this

TThreadWithYield
1
2
3
4
5
Finished

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM