Ожидание завершения TThread sh с WaitForSingleObject () - PullRequest
2 голосов
/ 03 апреля 2020

Я создаю поток, а затем говорю, что хочу дождаться его завершения вызовом WFSO (упрощенный псевдокод ниже, очевидно, никто не хочет ждать поток сразу после его создания).

constructor TFileScannerThread.Create(Parameters)
begin
/// assign parameters to private variables, and call
inherited Create(true);  //Create Suspended
FreeOnTerminate := true; 
Start; 
end;

В основном потоке

fst := TFileScannerThread.Create(BaseFolder, ScanMode, AbortEvent);

///  I presume, the next call would block the main thread until the child thread is done
///  but, it seems to create a deadlock & WFSO never returns
///  I get a WAIT_TIMEOUT if I use a finite wait time
WaitForsingleObject(fst.Handle, INFINITE);

Что я делаю неправильно / отсутствует? Если у меня нет WFSO, поток завершается примерно через 10 секунд.

Редактировать: Создание с FreeOnTerminate = false не создает эту проблему.

1 Ответ

6 голосов
/ 04 апреля 2020

Ожидание объекта TThread, использующего FreeOnTerminate=true, является условием гонки, поскольку объект может быть освобожден в в любой момент после выхода из метода Execute(). Поэтому, если вы не можете гарантировать , что вы вызываете WaitForSingleObject() до события OnTerminate потока, вы не гарантируете наличие действительного объекта на котором читать его Handle свойство. По сути, после выхода из конструктора потока все ставки отключаются при использовании FreeOnTerminate=true, если только вы не используете обработчик событий OnTerminate. FreeOnTerminate=true предназначен для создания тем типа "забудь". Если вам по какой-либо причине необходимо обратиться к потоку, использование FreeOnTerminate=true становится опасным, если вы не очень осторожны.

Если вы собираетесь ждать объект TThread, нет смысла использовать FreeOnTerminate=true для этого объекта, так как вы можете просто освободить объект самостоятельно после завершения ожидания. Это гарантирует, что объект останется действительным, пока вы не освободите его вручную, поэтому вы можете использовать его Handle в любое время:

constructor TFileScannerThread.Create(Parameters)
begin
  inherited Create(False);
  FreeOnTerminate := false;
  // assign parameters to private variables
end;
fst := TFileScannerThread.Create(BaseFolder, ScanMode, AbortEvent);
...
WaitForSingleObject(fst.Handle, INFINITE);
fst.Free;

(вам не нужно звонить Start() внутри конструктора, используйте CreateSuspended=False, вместо этого поток фактически не начнет работать, пока не выйдет конструктор)

При этом WaitForSingleObject() невозможно вернуть WAIT_TIMEOUT в INFINITE тайм-аут. Но он может вернуть WAIT_FAILED, например, когда объект TThread освобожден до вызова WaitForSingleObject(), или даже когда он все еще ожидает на Handle, таким образом закрывая Handle, пока он

Что касается тупика, если у вас есть OnTerminate обработчик событий, назначенный объекту TThread, этот обработчик вызывается с помощью метода TThread::Synchronize(), так что обработчик запускается в Основная тема. Но если основной поток заблокирован на WaitForSingleObject(), он не сможет обслуживать запросы TThread::Synchronize() (или TThread::Queue()). Таким образом, в итоге рабочий поток ожидает в основном потоке, в то время как основной поток ожидает в рабочем потоке - тупик.

Чтобы избежать этого, вы можете вызвать WaitForSingleObject() в al oop с помощью короткий тайм-аут, так что вы можете периодически вызывать функцию RTL CheckSynchronize() во время ожидания:

var h: THandle;

h := fst.Handle;
while WaitForSingleObject(h, 500) = WAIT_TIMEOUT do
  CheckSynchronize;

fst.Free;

При использовании блокирующего ожидания возникают другие проблемы, такие как SendMessage() звонки из других тем в основной поток. Таким образом, вам также необходимо обслуживать эти запросы:

var
  h: THandle;
  ret: DWORD;
  msg: TMsg;

h := fst.Handle;
repeat
  case MsgWaitForMultipleObjects(1, h, False, 1000, QS_SENDMESSAGE) of
    WAIT_OBJECT_0, WAIT_FAILED: Break;
    WAIT_OBJECT_0 + 1: PeekMessage(msg, 0, 0, 0, PM_NOREMOVE);
    WAIT_TIMEOUT: CheckSynchronize;
  end;
until False;

fst.Free;

В качестве альтернативы, добавьте дескриптор Classes.SyncEvent к ожиданию, а также (TThread::Synchronize() и TThread::Queue() сигнализируют его внутренне когда есть ожидающие запросы):

var
  h: array[0..1] of THandle;
  ret: DWORD;
  msg: TMsg;

h[0] := fst.Handle;
h[1] := SyncEvent;
repeat
  case MsgWaitForMultipleObjects(2, h, False, INFINITE, QS_SENDMESSAGE) of
    WAIT_OBJECT_0, WAIT_FAILED: Break;
    WAIT_OBJECT_0 + 1: CheckSynchronize;
    WAIT_OBJECT_0 + 2: PeekMessage(msg, 0, 0, 0, PM_NOREMOVE);
  end;
until False;

fst.Free;

К вашему сведению, TThread имеет свой собственный метод WaitFor(), который выполняет ожидание блокировки потока для завершения при обслуживании TThread::Synchronize() / TThread::Queue() и SendMessage() запросов, аналогичных приведенным выше:

fst := TFileScannerThread.Create(BaseFolder, ScanMode, AbortEvent);
...
fst.WaitFor;
fst.Free;

Просто обратите внимание, что вызов TThread::WaitFor() для TThread объекта, который использует FreeOnTerminate=true, также небезопасен. Он либо завершится с ошибкой EThread или EOSError, когда Handle будет закрыт между внутренними итерациями l oop, либо, скорее всего, просто взломает sh сразу, когда попытается получить доступ к Handle TThread объект, который уже был освобожден.

...