Многопоточные программы в Delphi изнутри

                                                                                               Автор: Андрей Боровский, Delphiplus

Потоки появились еще в Windows NT, но до определенного времени редко использовались прикладными программистами. В наше время, когда даже самый захудалый офисный компьютер обладает как минимум двумя процессорными ядрами, не использовать потоки в программах просто неприлично. В этой статье мы рассмотрим реализацию многопоточности в Delphi 6, Delphi 7 и Delphi 2009. Базовые принципы работы с классом TThread рассматривать не будем, для этого есть встроенная документация. Мы же посмотрим, что у системы под капотом. В качестве введения я кратко опишу две основные проблемы, возникающие в многопоточном программировании.

В процессе разработки многопоточного приложения приходится решать две взаимосвязанные проблемы — разграничение доступа к ресурсам и взаимоблокировки. Если несколько потоков обращаются к одному и тому же ресурсу (области памяти, файлу, устройству) при небрежном программировании может возникнуть ситуация, когда сразу несколько потоков попытаются выполнить некие манипуляции с общим ресурсом. При этом нормальная последовательность операций при обращении к ресурсу, скорее всего, будет нарушена. Проблема с разграничением доступа может возникнуть даже при очень простых операциях. Предположим, у нас есть программа, которая создает несколько потоков. Каждый поток выполняет свою задачу, и затем завершается. Мы хотим контролировать количество потоков, активных в данное время, и с этой целью вводим счетчик потоков — глобальную переменную Counter. Процедура потока при этом может выглядеть так: 
procedure MyThread.Execute;
begin
  Inc(Counter);
  …
  Dec(Counter);
end; 

Одна из проблем, связанных с этим кодом заключается в том, что процедуры Inc и Dec не атомарны (например, процедуре Inc требуется выполнить три инструкции процессора — загрузить значение Counter в регистр процессора, выполнить сам инкремент, затем записать результат из регистра процессора в область памяти Counter). Нетрудно догадаться, что если два потока попытаются выполнить процедуру Inc одновременно, значение Counter может быть увеличено на 1 вместо 2. Такие ошибки трудно обнаружить при отладке, так как вероятность ошибки не равна единице. Волне может случиться так, что при тестовом прогоне программы все будет работать нормально, а ошибку обнаружит уже заказчик…

Решением проблемы может стать использование специальных функций. Например, вместо процедуры Inc можно использовать процедуру Win API InterlockedIncrement, а вместо Dec — InterlockedDecrement. Эти процедуры гарантируют, что в любой момент времени не более одного потока получит доступ к переменной-счетчику.

В общем случае для решения проблемы разделения доступа используются блокировки — специальные механизмы, которые гарантируют, что в каждый данный момент времени только один поток имеет доступ к некоторому ресурсу. В то время, когда ресурс заблокирован одним потоком, выполнение других потоков, пытающихся получить доступ к ресурсу, приостанавливается. Однако использование блокировок создает другую проблему — взаимоблокировки (deadlocks). Взаимоблокировка наступает тогда, когда потока A блокирует ресурс, необходимый для продолжения работы потока B, а поток B блокирует ресурс, необходимый для продолжения работы потока A. Поскольку ни один поток не может продолжить выполнение, заблокированные ресурсы не могут быть разблокированы, что приводит к повисанию потоков.
Потоки в Delphi 6

Из всех рассмотренных реализаций, реализация потоков в Delphi 6 самая простая. Это как бы основа, на которой более поздние версии строят более сложную модель взаимодействия потоков.

В Delphi 6, как, впрочем, и в других версиях Delphi, В качестве функции потока используется функция ThreadProc из модуля Classes, которая, в свою очередь, вызывает метод Execute объекта TThread: 
function ThreadProc(Thread: TThread): Integer;
var
  FreeThread: Boolean;
begin
  try
  if not Thread.Terminated then
  try
  Thread.Execute;
  except
  Thread.FFatalException := AcquireExceptionObject;
  end;
  finally
  FreeThread := Thread.FFreeOnTerminate;
  Result := Thread.FReturnValue;
  Thread.FFinished := True;
  Thread.DoTerminate;
  if FreeThread then Thread.Free;
  EndThread(Result);
  end;
end; 

С помощью функции AcquireExceptionObject ThreadProc получает адрес объекта исключения, буде таковое возникнет в методе Execute, и сохраняет его в переменной FFatalException, так что потом мы сможем получить этот адрес с помощью свойства FatalException объекта TThread. Вызов AcquireExceptionObject приводит к тому, что объект исключения не будет уничтожен на выходе из блока except. Метод DoTerminate вызывает обработчик события OnTerminate, если таковой назначен. Тут интересно отметить, что обработчик OnTerminate вызывается до фактического завершения процедуры потока (хотя и после выхода из метода Execute). Функция EndThread вызывает функцию, адрес которой присвоен переменной System.SystemThreadEndProc. Эта переменная используется библиотекой времени выполнения C++Builder, в Delphi же ей не присваивается никакого значения, так что мы можем использовать переменную SystemThreadEndProc в наших собственных целях (об этом будет подробнее ниже).

Важную роль при работе с потоками играет метод Synchronize. Этот метод гарантирует, что метод, адрес которого передается ему в качестве параметра, будет выполнен основным потоком и не прервет выполнение других методов, выполняемых в основном потоке, в неподходящем месте. В Delphi 6 метод Synchronize реализован следующим образом: 
procedure TThread.Synchronize(Method: TThreadMethod);
var
  SyncProc: TSyncProc;
begin
  SyncProc.Signal := CreateEvent(nil, True, False, nil);
  try
  EnterCriticalSection(ThreadLock);
  try
  FSynchronizeException := nil;
  FMethod := Method;
  SyncProc.Thread := Self;
  SyncList.Add(@SyncProc);
  ProcPosted := True;
  if Assigned(WakeMainThread) then
  WakeMainThread(Self);
  LeaveCriticalSection(ThreadLock);
  try
  WaitForSingleObject(SyncProc.Signal, INFINITE);
  finally
  EnterCriticalSection(ThreadLock);
  end;
  finally
  LeaveCriticalSection(ThreadLock);
  end;
  finally
  CloseHandle(SyncProc.Signal);
  end;
  if Assigned(FSynchronizeException) then raise FSynchronizeException;
end; 

Структура TSyncProc имеет вид: 
TSyncProc = record
  Thread: TThread;
  Signal: THandle;
end; 

Она содержит информацию о потоке, вызвавшем Synchronize, и идентификатор сигнала, который используется для синхронизации Synchronize (простите за невольный каламбур). 

Чтобы разобраться в работе метода Synchronize, следует понять механизм обработки методов, которые вторичные потоки хотят вызвать в контексте главного потока. Поток, желающий выполнить метод в контексте главного потока, создает сигнал SyncProc.Signal (в сброшенном состоянии), записывает адрес вызвавшего его объекта TThread в SyncProc.Thread, адрес вызываемого метода — в переменную Self.FMethod (что соответствует SyncProc.Thread.FMethod) и помещает указатель на переменную SyncProc в специальный список SyncList класса TList. Далее метод ждет, пока цикл обработки сообщений главного потока не установит сигнал SyncProc.Signal. В свое время цикл обработки сообщение главного потока считает запись SyncProc из списка SyncList, выполнит метод SyncProc.Thread.FMethod, после чего установит сигнал SyncProc.Signal. В результате метод Synchronize, ожидающий SyncProc.Signal, вернет управление вызвавшему его потоку после того, как будет выполнен метод SyncProc.Thread.FMethod. Критическая секция ThreadLock гарантирует, что в каждый момент времени не более чем один поток получит доступ к объекту SyncList. Таким образом, вызов метода Synchronize в потоке блокирует выполнение этого потока до тех пор, пока главный поток не выполнит метод Method, но не блокирует вызов Synchronize другими потоками (все методы, ожидающие выполнения, накапливаются в списке SyncList). Глобальный флаг ProcPosted устанавливается методом Synchronize и информирует заинтересованные потоки о том, что как минимум один метод ожидает выполнения в очереди. Интересно отметить, что адрес метода Method сохраняется не в структуре SyncProc, а в поле объекта TThread, вызвавшего Synchronize. В Delphi 6 это не вызывает проблем, поскольку Synchronize блокирует вызывающий поток до выполнения Method, а значит ни один поток не может поместить в очередь SyncList более одного метода одновременно. Однако в последующих версиях Delphi это поведение было изменено.

Важно подчеркнуть, что метод Synchronize не просто синхронизирует выполнение переданного ему метода Method с выполнением других методов в главном потоке, но и организует выполнение Method в контексте главного потока. То есть, например, глобальные переменные, объявленные как threadvar, при выполнении Method будут иметь значения, присвоенные им в главном потоке, а не те, которые присвоены им в потоке, вызвавшем Synchronize.

Для нормальной синхронизации главного и вторичного потока метода Synchronize недостаточно. Представьте себе такую ситуацию: в методе главного потока мы ожидаем завершения вторичного потока (не важно, каким образом, важно то, что этот метод не вернет управление главному потоку до тех пор, пока вторичный поток не завершится), а в это время вторичный поток вызывает метод Synchronize. В результате возникнет взаимоблокировка: метод главного потока не может завершиться пока не завершится вторичный поток, а вторичный поток не может завершиться, пока не будет выполнен метод Synchronize (а для этого нужно, чтобы главный поток вернулся в цикл обработки сообщений). Для разрешения этой ситуации существует функция CheckSynchronize, вызов которой приводит к выполнению всех методов, находящихся в данный момент в очереди SyncList, и возвращению управления из всех методов Synchronize, вызванных вторичными потоками. В Delphi 6 эта функция реализована следующим образом: 
function CheckSynchronize: Boolean;
var
  SyncProc: PSyncProc;
begin
  if GetCurrentThreadID <> MainThreadID then
  raise EThread.CreateResFmt(@SCheckSynchronizeError, [GetCurrentThreadID]);
  if ProcPosted then
  begin
  EnterCriticalSection(ThreadLock);
  try
  Result := (SyncList <> nil) and (SyncList.Count > 0);
  if Result then
  begin
  while SyncList.Count > 0 do
  begin
  SyncProc := SyncList[0];
  SyncList.Delete(0);
  try
  SyncProc.Thread.FMethod;
  except
  SyncProc.Thread.FSynchronizeException := AcquireExceptionObject;
  end;
  SetEvent(SyncProc.signal);
  end;
  ProcPosted := False;
  end;
  finally
  LeaveCriticalSection(ThreadLock);
  end;
  end else Result := False;
end; 

Как видим, функция CheckSynchronize работает просто: она проверяет значение ProcPosted, и, если это значение равно True, последовательно изымает записи из очереди SyncList, выполняет соответствующие методы и устанавливает соответствующие сигналы. Обратите внимание на то, что функция проверяет (с помощью GetCurrentThreadID), из какого потока она вызвана. Вызов CheckSynchronize не из главного потока может привести к хаосу, так что в этом случае генерируется исключение EThread. Для обработки вызовов методов, встроенных в главный поток с помощью Synchronize, цикл обработки сообщений главного потока также вызывает метод CheckSynchronize.

Еще один интересный для нас метод — метод WaitFor класса TThread. Этот метод блокирует выполнение вызвавшего его потока до тех пор, пока не завершится поток, для объекта которого WaitFor был вызван. Проще говоря, если в главном потоке мы хотим дождаться завершения потока MyThread, мы можем вызвать 
MyThread.WaitFor; 

Вот как реализован WaitFor в Delphi 6: 
function TThread.WaitFor: LongWord;
var
  H: THandle;
  WaitResult: Cardinal;
  Msg: TMsg;
begin
  H := FHandle;
  if GetCurrentThreadID = MainThreadID then
  begin
  WaitResult := 0;
  repeat
  if WaitResult = WAIT_OBJECT_0 + 1 then
  PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE);
  Sleep(0);
  CheckSynchronize;
  WaitResult := MsgWaitForMultipleObjects(1, H, False, 0, QS_SENDMESSAGE);
  Win32Check(WaitResult <> WAIT_FAILED);
  until WaitResult = WAIT_OBJECT_0;
  end else WaitForSingleObject(H, INFINITE);
  CheckThreadError(GetExitCodeThread(H, Result));
end; 

Метод WaitFor может быть вызван не только из главного потока, но и из любого другого. Если WaitFor вызван из главного потока GetCurrentThreadID = MainThreadID, метод периодически вызывает функцию CheckSynchronize для предотвращения описанной выше взаимоблокировки, а так же периодически опустошает очередь сообщений Windows. Если метод WaitFor вызван для какого либо другого потока, у которого как предполагается, собственной очереди сообщений быть не может, он просто ждет сигнала о завершении подконтрольного потока с помощью функции Win API WaitForSingleObject. Тут надо отметить, что идентификатор потока (поле FHandle) является по совместительству сигналом, который устанавливается системой Windows при завершении работы потока. 

А что будет, если поток вызовет WaitFor для самого себя? Поток, вызвавший 
Self.WaitFor; 

будет вечно ожидать своего собственного завершения (по крайней мере, до тех пор, пока какой-то другой поток не вызовет жесткую функцию Win API TerminateThread для данного потока). Конечно, вряд ли здравомыслящий программист напишет что-то типа Self.WaitFor, но ситуации могут быть и более сложными. Странно, что разработчики Delphi не предотвратили такую возможность самоубийства, а ведь сделать это очень просто — достаточно сравнить значение FHandle и значение, возвращенное функцией GetCurrentThreadID. 
Потоки в Delphi 7

По сравнению с Delphi 6 изменений в работе с потоками в Delphi 7 не так уж и много. Рассмотрим реализацию функции CheckSynchronize: 
function CheckSynchronize(Timeout: Integer = 0): Boolean;
var
  SyncProc: PSyncProc;
  LocalSyncList: TList;
begin
  if GetCurrentThreadID <> MainThreadID then
  raise EThread.CreateResFmt(@SCheckSynchronizeError, [GetCurrentThreadID]);
  if Timeout > 0 then
  WaitForSyncEvent(Timeout)
  else
  ResetSyncEvent;
  LocalSyncList := nil;
  EnterCriticalSection(ThreadLock);
  try
  Integer(LocalSyncList) := InterlockedExchange(Integer(SyncList), Integer(LocalSyncList));
  try
  Result := (LocalSyncList <> nil) and (LocalSyncList.Count > 0);
  if Result then
  begin
  while LocalSyncList.Count > 0 do
  begin
  SyncProc := LocalSyncList[0];
  LocalSyncList.Delete(0);
  LeaveCriticalSection(ThreadLock);
  try
  try
  SyncProc.SyncRec.FMethod;
  except
  SyncProc.SyncRec.FSynchronizeException := AcquireExceptionObject;
  end;
  finally
  EnterCriticalSection(ThreadLock);
  end;
  SetEvent(SyncProc.signal);
  end;
  end;
  finally
  LocalSyncList.Free;
  end;
  finally
  LeaveCriticalSection(ThreadLock);
  end;
end; 

В новой версии вместо флага ProcPosted используется событие SyncEvent, для управления которым создано несколько функций: SetSyncEvent, ResetSyncEvent, WaitForSyncEvent. Метод WaitFor использует событие SyncEvent для оптимизации цикла обработки сообщений. Установка SyncEvent сигнализирует о том, что в очереди появился новый метод, ожидающий синхронизации, и требуется вызвать CheckSynchronize.

У метода CheckSynchronize появился параметр TimeOut, который указывает, сколько времени метод должен ждать события SyncEvent, прежде чем вернуть управление. Указывать время ожидания удобно там, где метод CheckSynchronize вызывается в цикле (при этом поток, вызывавший CheckSynchronize, отдает свое процессорное время другим потокам, вместо того, чтобы крутить вызовы вхолостую), однако и продолжительность вызова метода CheckSynchronize может неоправданно возрасти. Обратите внимание так же на то, как в Delphi 7 изменилась работа с очередью SyncList. В предыдущей версии CheckSynchronize очередь SyncList захватывалась (с помощью ThreadLock) на все время обработки помещенных в очередь методов (а это время могло быть сравнительно большим). А ведь пока CheckSynchronize владеет объектом SyncList, операции с очередью SyncList, выполняемые из других потоков, блокируются. Для того чтобы высвободить SyncList как можно скорее, сохраняет указатель на текущий объект очереди (с помощью функции Win API InterlockedExchange) в локальной переменной LocalSyncList, а переменной SyncList присваивает значение nil. После этого доступ к переменной SyncList открывается снова. Теперь, если другой поток захочет снова синхронизировать метод, ему понадобится создать новый объект SyncList, однако доступ к очереди блокируется только на время, необходимое для обмена указателями, так что общий выигрыш производительности должен быть значителен.
Потоки в Delphi 2009

По сравнению с Delphi 7 в классы потоков Delphi 2009 внесено много изменений. Прежде всего, в класс TThread добавлена группа перегруженных методов Queue, которые позволяют синхронизировать вызов методов с главным потоком в неблокирующем режиме. Напомню, что метод Synchronize приостанавливает выполнение вызывающего потока до тех пор, пока метод, переданный Synchronize, не будет синхронизирован с главным потоком. Фактически, Synchronize синхронизирует событие вызова метода в обоих потоках. Иногда это именно то, что нам нужно, но как правило нам все же не требуется дожидаться выполнения синхронизируемого метода в главном потоке. Кроме того, блокирующий метод Synchronize может стать источником серьезных проблем, если синхронизируемый метод как-то связан с действиями пользователя. Если синхронизируемому методу нужно дождаться ответа от пользователя через один из элементов пользовательского интерфейса, поток, вызвавший Synchronize, будет заблокирован на непозволительно долгий период времени. Как и метод Synchronize, метод Queue переводит переданный ему в качестве параметра метод в контекст главного потока и синхронизирует его с вызовами других методов главного потока, но при этом поток, вызвавший Queue, не ждет завершения выполнения синхронизируемого метода, а сразу же возобновляет работу. Я буду называть такой вызов метода в главном потоке асинхронным. Второе важное новшество заключается в том, что теперь можно синхронизировать с главным потоком не только метод класса, но и самостоятельную процедуру.

Рассмотрим реализации. Функция ThreadProc по сравнению с реализацией в Delphi 7 практически не изменилась и мы на ней останавливаться не будем. Рассмотрим реализацию метода Synchronize. У этого метода в Delphi 2009 много перегруженных вариантов, но все они, в конечном итоге, вызывают следующий метод: 
class procedure TThread.Synchronize(ASyncRec: PSynchronizeRecord; QueueEvent: Boolean = False);
var
  SyncProc: TSyncProc;
  SyncProcPtr: PSyncProc;
begin
  if GetCurrentThreadID = MainThreadID then
  begin
  if Assigned(ASyncRec.FMethod) then
  ASyncRec.FMethod()
  else if Assigned(ASyncRec.FProcedure) then
  ASyncRec.FProcedure(); 
  end 
  else begin
  if QueueEvent then
  New(SyncProcPtr)
  else
  SyncProcPtr := @SyncProc;
  if not QueueEvent then
  SyncProcPtr.Signal := CreateEvent(nil, True, False, nil)
  else
  SyncProcPtr.Signal := 0;
  try
  EnterCriticalSection(ThreadLock);
  try
  SyncProcPtr.Queued := QueueEvent;
  if SyncList = nil then
  SyncList := TList.Create;
  SyncProcPtr.SyncRec := ASyncRec;
  SyncList.Add(SyncProcPtr);
  SignalSyncEvent;
  if Assigned(WakeMainThread) then
  WakeMainThread(SyncProcPtr.SyncRec.FThread);
  if not QueueEvent then
  begin
  LeaveCriticalSection(ThreadLock);
  try
  WaitForSingleObject(SyncProcPtr.Signal, INFINITE);
  finally
  EnterCriticalSection(ThreadLock);
  end;
  end;
  finally
  LeaveCriticalSection(ThreadLock);
  end;
  finally
  if not QueueEvent then
  CloseHandle(SyncProcPtr.Signal);
  end;
  if not QueueEvent and Assigned(ASyncRec.FSynchronizeException) then
  raise ASyncRec.FSynchronizeException;
  end;
end; 

Параметр ASyncRec представляет собой указатель на структуру TSynchronizeRecord, в которой хранится адрес вызываемой процедуры или метода и объекта потока. Параметр QueueEvent указывает, должен ли метод Synchronize вызываться в блокирующем (значение False) или неблокирующем (значение True) режиме, предназначенном для асинхронных вызовов. Метод Queue так же использует эту версию Synchronize, передавая во втором параметре False.

Работа метода в блокирующем режиме похожа на работу метода Synchronize в Delphi 7: система создает событие SyncProc.Signal, которое будет сигнализировать о выполнении метода в главном потоке, затем формирует структуру SyncProc, описывающую синхронизируемый метод, добавляет эту структуру в очередь SyncList, устанавливает сигнал SyncEvent и ждет, пока функция CheckSynchronize не установит сигнал SyncProc.Signal, свидетельствующий о том, что синхронизируемый метод выполнен. Для описания вызываемого метода по-прежнему используется запись типа TSyncProc, которая, однако, выглядит по-другому: 
TSyncProc = record
  SyncRec: PSynchronizeRecord;
  Queued: Boolean;
  Signal: THandle;
end; 

Поле SyncRec представляет собой указатель на структуру TSynchronizeRecord. Поле Queued указывает, является ли вызов асинхронным, а поле Signal используется при блокирующем вызове.

Если в параметре QueueEvent передается значение True, вызов метода добавляется в очередь асинхронно. В этом случае мы создаем новый экземпляр записи TSyncProc (для асинхронного вызова нельзя использовать локальную переменную, поскольку структура должна существовать после завершения вызова Synchronize).

Отметим, что в Delphi 2009 один поток может поместить в очередь сразу несколько асинхронных вызовов методов или процедур, а значит, адрес метода или процедуры уже нельзя хранить в поле объекта TThread. Теперь этот адрес хранится в структуре, описывающей вызов. Следует помнить о важном различии между блокирующим и асинхронным вызовом Synchronize. При блокирующем вызове жизненным циклом записи, адрес которой передается в параметре ASyncRec, управляет вызывающий поток. В случае асинхронного вызова экземпляр записи, на которую указывает ASyncRec, уничтожается методом CheckSynchronize, что происходит в другом потоке (и, скорее всего, после возврата из Synchronize). Таким образом, если при блокирующем вызове Synchronize (параметр QueueEvent равен False) параметр ASyncRec может ссылаться на локальную переменную, при асинхронном вызове (параметр QueueEvent равен True) параметр ASyncRec должен ссылаться на блок динамической памяти и не следует пытаться удалить этот блок после возврата из Synchronize.
Недостатки реализации потоков в Delphi

Самым главным недостатком следует признать метод, применяемый для приостановки и возобновления выполнения потока. С этой целью в VCL используются функции Windows API SuspendThread и ResumeThread, которые вообще говоря. Предназначены для отладочных целей. Функция SuspendThread может остановить выполнение потока в любой точке. Поток не может запретить приостановку на время выполнения критического фрагмента кода и не получает оповещения о том, что он будет приостановлен. Обмен сообщениями между вторичными потоками и главным потоком продуман достаточно хорошо, в последних версиях Delphi добавлены даже асинхронные вызовы, а вот стандартных механизмов передачи сообщений от главного потока к второстепенным не существует. Тут надо отметить, что под главным потоком мы понимаем поток, в котором выполняется метод Application.Run, и обрабатываются события. Delphi плохо подходит для модели, в которой все потоки равноправны.
Расширение возможностей

В качестве демонстрации низкоуровневой работы с очередью вызовов в программе Delphi, напишем функцию ExecAfter. Иногда нам бывает нужно указать, чтобы некоторая процедура выполнялась после выхода из той процедуры, в которой мы находимся. Обычно для решения этой задачи с помощью функции Win API PostMessage главному окну посылается какое-нибудь пользовательское сообщение, обработчик которого вызывает требуемую процедуру. Недостатком этого решения является то, что оно работает только в графических программах, и нам приходится определять собственную обработку сообщений, которые получает окно. В то же время структура очереди сообщений Delphi 2009 позволяет нам поместить в очередь асинхронный вызов, который будет выполнен при следующем вызове CheckSynchronize, то есть, после выхода из процедуры, в которой мы сейчас находимся. Может возникнуть соблазн воспользоваться вызовом 
TThread.Queue(nil, MethodToExecute); 

но в главном потоке это не сработает. Метод Synchronize, который вызывает Queue, проверит, не вызван ли он из главного потока и в этом случае выполнит MethodToExecute, не откладывая. Итак, процедура ExecAfter: 
procedure ExecAfter(AMethod : TThreadMethod);
var
  SyncProcPtr: PSyncProc;
  SyncRecPtr: PSynchronizeRecord;
begin
  New(SyncProcPtr);
  New(SyncRecPtr);
  SyncRecPtr.FThread := nil;
  SyncRecPtr.FMethod := AMethod;
  SyncRecPtr.FProcedure := nil;
  SyncRecPtr.FSynchronizeException := nil;
  SyncProcPtr.Signal := 0;
  EnterCriticalSection(ThreadLock);
  try
  SyncProcPtr.Queued := True;
  if SyncList = nil then
  SyncList := TList.Create;
  SyncProcPtr.SyncRec := SyncRecPtr;
  SyncList.Add(SyncProcPtr);
  SignalSyncEvent;
  if Assigned(WakeMainThread) then
  WakeMainThread(SyncProcPtr.SyncRec.FThread);
  finally
  LeaveCriticalSection(ThreadLock);
  end;
end; 

Наш вариант ExecAfter позволяет выполнить вызов метода AMethod после выхода из той процедуры, в которой мы вызвали ExecAfter (при желании процедуру нетрудно переписать для вызова самостоятельных процедур, а не методов). Реализация процедуры ExecAfter должна быть расположена в модуле Classes, иначе мы не сможем получить доступ к необходимым нам переменным ThreadLock и SyncList. Между прочим, если вы не хотите вносить изменения в главную копию Classes, вы можете использовать локальную копию для конкретной программы. Для того скопируйте измененный файл Classes.pas в директорию проекта. Теперь, если добавить в программу последовательность: 
TForm1.Method1;
begin
  ExecAfter(Method2);
  Method3;
end; 

Method2 будет выполнен после Method3 (и после выхода из Method1). 

По адресу http://symmetrica.net/Delphi/JoinableThreads.htm вы найдете еще одно расширение — модуль JoinableThreads, который реализует производный от TThread класс TJoinableThread и функцию Join, которая представляет собой аналог WaitFor, позволяющий дождаться завершения всех ранее созданных потоков TJoinableThread.