Выполнение кода в потоке без выделения его в процедуру

Источник: delphikingdom
Александр Алексеев

Автор: © Александр Алексеев

Вашему вниманию (читай: для использования и тестирования) предлагается модуль TasksEx.pas, который предлагает всего две функции:

type
  TContext = type Integer;

function  EnterWorkerThread: TContext;
procedure LeaveWorkerThread(Context: TContext);

Код, помещённый между вызовами EnterWorkerThread и LeaveWorkerThread , будет выполняться как если бы он был помещён в метод TThread.Execute .

Для начала выполнения кода в другом потоке просто вставьте в код вызов EnterWorkerThread . Для обратного переключения нужно использовать LeaveWorkerThread . Эти вызовы могут быть вложенными, но _обязаны_ быть парными. Также они должны вызываться из одной и той же процедуры/функции/метода.

В процедуру LeaveWorkerThread нужно передать результат выполнения функции EnterWorkerThread . Это значение позволяет отличать несколько вызовов EnterWorkerThread друг от друга (поскольку функцию EnterWorkerThread можно вызывать многократно). Вызывающая сторона должна трактовать это значение как простое число и не должна пытаться как-либо интерпретировать его или делать предположение о его содержимом. Всё что она может сделать с ним - передать его в парный вызов LeaveWorkerThread и забыть про него после вызова.

Рекомендованная конструкция:

var
  Context: TContext;
begin

  // Этот код выполняется в главном потоке (например, Button1Click)

  Context := EnterWorkerThread;
  try

    { Этот код выполняется во вторичном потоке }
    { Хотя это код Button1Click, но он выполняется, как если бы он был }
    { помещён в TThread.Execute. }

  finally
    LeaveWorkerThread(Context);
    // После вызова LeaveWorkerThread переменная Context недействительна.
  end;

  // Этот код выполняется в главном потоке

end;

Весь код между EnterWorkerThread и LeaveWorkerThread выполняется во вторичном потоке. Вторичный поток выбирается случайным образом из пула свободных рабочих потоков (используется модуль AsyncCalls , см. ниже). Если таковых нет, то выполнение кода откладывается (добавляется в очередь для исполнения), пока не освободится один из рабочих потоков. Число потоков в пуле контролируется Get/SetMaxAsyncCallThreads . По-умолчанию их не меньше двух.

Во время выполнения кода во вторичном потоке или во время ожидания освобождения рабочего потока главный поток находится в цикле вызовов Application.HandleMessage . Во время этого цикла может быть вызван код, вызывающий EnterWorkerThread/LeaveWorkerThread . Поэтому в любой момент времени одновременно может выполняться несколько блоков кода между EnterWorkerThread и LeaveWorkerThread , в том числе и несколько вызовов одного и того же кода. Число одновременно выполняющихся блоков ограничено числом потоков в пуле потоков. В таких случаях выполнение кода после первого LeaveWorkerThread продолжится только после того, как все вложенные вызовы будут завершены.

Например (// - главный поток, {} - вторичные потоки ):

// вызван обработчик события Button1Click

EnterWorkerThread #1

// главный поток крутится в цикле с Application.HandleMessage
{ вторичный поток обрабатывает блок  1 }

// во время Application.HandleMessage в главном потоке происходит вызов
// EnterWorkerThread при обработке какого-либо события (например,
// Button2Click или снова Button1Click):

  EnterWorkerThread #2
  // главный поток крутится во вложенном цикле с Application.HandleMessage
  { два вторичных потока выполняют блоки  1 и  2 }
  { блок  2 = блоку  1, если повторно был вызван обработчик Button1Click }

  { блок  1 завершил выполнение, }
  { но код после LeaveWorkerThread  1 не может начать выполнение, }
  { т.к. код главного потока крутится во вложенном цикле с }
  { Application.HandleMessage }
  { поэтому идёт ожидание завершения блока  2 }

  { блок  2 завершил выполнение }

  LeaveWorkerThread #2

  // выполняется код после LeaveWorkerThread  2

  // выход из обработчика событий, Application.HandleMessage возвращает
  // управление

// остановка вложенного цикла Application.HandleMessage, т.к. блок  1
// завершил выполнение

// выполняется код после LeaveWorkerThread  1

Если блок 2 закончит работу раньше, чем блок 1, то ожидания не происходит.

Например:

// вызван обработчик события Button1Click

EnterWorkerThread #1

// главный поток крутится в цикле с Application.HandleMessage
{ вторичный поток обрабатывает блок  1 }

// во время Application.HandleMessage в главном потоке происходит вызов
// EnterWorkerThread при обработке какого-либо события:

  EnterWorkerThread #2
  // главный поток крутится во вложенном цикле с Application.HandleMessage
  { два вторичных потока выполняют блоки  1 и  2 }

  { блок  2 завершил выполнение }
  { вторичный поток обрабатывает блок  1 }

  LeaveWorkerThread #2

  // выполняется код после LeaveWorkerThread  2
  { вторичный поток обрабатывает блок  1 }

  // выход из обработчика событий, Application.HandleMessage возвращает
  // управление

// главный поток крутится в цикле с Application.HandleMessage
{ вторичный поток обрабатывает блок  1 }

{ блок  1 завершил выполнение }

LeaveWorkerThread #1

// выполняется код после LeaveWorkerThread  1

Ситуация аналогична тому, как в однопоточном приложении во время вызова Application.ProcessMessages вызывается длительный обработчик события, и Application.ProcessMessages не возвращает управления, пока обработчик не закончит работу. Эту ситуацию не следует путать с вложенным вызовом EnterWorkerThread :

...

Context := EnterWorkerThread;
try
  ...
  P;
  ...
finally
  LeaveWorkerThread(Context);
end;

...

procedure P;
var
  Context: TContext;
begin
  Context := EnterWorkerThread; // ничего не делает,
                                // т.к. мы уже во вторичном потоке
  try
    ...
  finally
    LeaveWorkerThread(Context); // ничего не делает, т.к. EnterWorkerThread
                                // ничего не делал
  end;
end;

При выходе из приложения выход откладывается, пока не будут завершены все выполняющиеся блоки вызовов.

В коде между EnterWorkerThread и LeaveWorkerThread можно использовать все локальные и глобальные переменные текущей процедуры/функции и её параметры, а также локальные переменные и парметры процедуры/функции, в которую вложена текущая процедура/функция.

Исключения, возникшие в этом коде останавливают его выполнение и перевозбуждаются уже в главном потоке. При возникновении исключения LeaveWorkerThread ничего не делает, позволяя завершиться раскрутке исключения.

Поэтому:

// Главный поток
try
  // Главный поток
  C := EnterWorkerThread;
  { Вторичный поток }
  try
    { Вторичный поток }
  finally
    { Вторичный поток }
    LeaveWorkerThread(C);
    // Если исключение не возникло, то - главый поток
    { Если исключение возникло, то - вторичный поток }
  end;
  // Главный поток (пропускается, если возникло исключение)
finally
  // Главный поток
end;
// Главный поток (пропускается, если возникло исключение)

По этой причине не рекомендуется вставлять код в Finally -блок для LeaveWorkerThread .

Интегированный отладчик Delphi не способен следить за выполнением смены потоков. Если вы отлаживаетесь и хотите сделать Step Over для Enter/LeaveWorkerThread , то вы должны поставить breakpoint сразу после вызова этих функций.

При повторном вхождении в EnterWorkerThread рабочий поток не обязан быть тем же самым, что и при первом вызове. Например:

var
  Context: TContext;
begin

  // Этот код выполняется в главном потоке

  Context := EnterWorkerThread;
  try

    { Этот код выполняется во вторичном потоке  1 }

  finally
    LeaveWorkerThread(Context);
  end;

  // Этот код выполняется в главном потоке

  Context := EnterWorkerThread;
  try

    { Этот код выполняется во вторичном потоке  2, который может быть }
    { тем же самым вторичным потоком  1, а может быть и абсолютно другим. }

  finally
    LeaveWorkerThread(Context);
  end;

  // Этот код выполняется в главном потоке

end;

Для временного переключения в главный поток используйте функции из AsyncCalls EnterMainThread/LeaveMainThread . Для них справедливы все те же замечания, что и для EnterWorkerThread/LeaveWorkerThread . Например:

var
  Context: TContext;
begin

  // Этот код выполняется в главном потоке

  Context := EnterWorkerThread;
  try

    { Этот код выполняется во вторичном потоке  1 }

    EnterMainThread;
    try

      // Этот код выполняется в главном потоке

    finally
      LeaveMainThread;
    end;

    { Этот код выполняется во вторичном потоке  1 }

  finally
    LeaveWorkerThread(Context);
  end;

  // Этот код выполняется в главном потоке

end;

Вызов EnterMainThread/LeaveMainThread подобен вызову Synchronize . Поскольку одновременно в главном потоке может выполняться лишь один код, то EnterMainThread блокирует (вызовом EnterCriticalSection ) выполнение потока, если уже есть поток, вызвавший EnterMainThread и не вызвавший ещё LeaveMainThread . Поэтому EnterMainThread/LeaveMainThread не передают контекста, т.к. он сохраняется в глобальной переменной, поскольку в любой момент времени такой контекст может быть только один. Также, во время вызова EnterMainThread/LeaveMainThread рабочий поток ожидает завершения работы блока и не возвращается в пул свободных рабочих потоков.

Модуль TaskEx.pas в текущей версии пока не обеспечивает работу с run-time пакетами. Эта возможность в процессе разработки.

Скачать пример TasksEx.zip

Для работы модуля требуется модуль AsyncCalls (версии 2.0 или выше) от Andreas Hausladen , который можно взять тут: http://andy.jgknet.de/async/


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=16504