Александр Алексеев дата публикации 06-06-2008 05:46 Выполнение кода в потоке без выделения его в процедуру
Вашему вниманию (читай: для использования и тестирования) предлагается модуль TasksEx.pas, который предлагает всего две процедуры:
procedure EnterWorkerThread;
procedure LeaveWorkerThread;
|
|
Код, помещённый между вызовами EnterWorkerThread и LeaveWorkerThread, будет выполняться, как если бы он был помещён в метод TThread.Execute.
Для начала выполнения кода в другом потоке просто вставьте в код вызов EnterWorkerThread. Для обратного переключения нужно использовать LeaveWorkerThread. Эти вызовы могут быть вложенными, но обязаны быть парными. Также они должны вызываться из одной и той же процедуры/функции/метода.
Рекомендованная конструкция:
begin
EnterWorkerThread;
try
finally
LeaveWorkerThread;
end;
end;
|
|
Весь код между EnterWorkerThread и LeaveWorkerThread выполняется во вторичном потоке. Вторичный поток выбирается случайным образом из пула свободных рабочих потоков (используется модуль AsyncCalls, см. ниже). Если таковых нет, то выполнение кода откладывается (добавляется в очередь для исполнения), пока не освободится один из рабочих потоков. Число потоков в пуле контролируется Get/SetMaxAsyncCallThreads. По-умолчанию их не меньше двух.
Во время выполнения кода во вторичном потоке или во время ожидания освобождения рабочего потока главный поток находится в цикле вызовов Application.HandleMessage. Во время этого цикла может быть вызван код, вызывающий EnterWorkerThread/LeaveWorkerThread. Поэтому в любой момент времени одновременно может выполняться несколько блоков кода между EnterWorkerThread и LeaveWorkerThread, в том числе и несколько вызовов одного и того же кода. Число одновременно выполняющихся блоков ограничено числом потоков в пуле потоков. В таких случаях выполнение кода после первого LeaveWorkerThread продолжится только после того, как все вложенные вызовы будут завершены.
Например (// — главный поток, {} — вторичные потоки ):
EnterWorkerThread #1
EnterWorkerThread #2
LeaveWorkerThread #2
|
|
Если блок 2 закончит работу раньше, чем блок 1, то ожидания не происходит.
Например:
EnterWorkerThread #1
EnterWorkerThread #2
LeaveWorkerThread #2
LeaveWorkerThread #1
|
|
Ситуация аналогична тому, как в однопоточном приложении во время вызова Application.ProcessMessages вызывается длительный обработчик события, и Application.ProcessMessages не возвращает управления, пока обработчик не закончит работу. Эту ситуацию не следует путать с вложенным вызовом EnterWorkerThread:
...
EnterWorkerThread;
try
...
P;
...
finally
LeaveWorkerThread;
end;
...
procedure P;
begin
EnterWorkerThread;
try
...
finally
LeaveWorkerThread;
end;
end;
|
|
При выходе из приложения выход откладывается, пока не будут завершены все выполняющиеся блоки вызовов.
В коде между EnterWorkerThread и LeaveWorkerThread можно использовать все локальные и глобальные переменные текущей процедуры/функции и её параметры, а также локальные переменные и параметры процедуры/функции, в которую вложена текущая процедура/функция.
Исключения, возникшие в этом коде, останавливают его выполнение и перевозбуждаются уже в главном потоке. При возникновении исключения LeaveWorkerThread ничего не делает, позволяя завершиться раскрутке исключения.
Поэтому:
try
EnterWorkerThread;
try
finally
LeaveWorkerThread;
end;
finally
end;
|
|
По этой причине не рекомендуется вставлять код в Finally-блок для LeaveWorkerThread.
Интегрированный отладчик Delphi не способен следить за выполнением смены потоков. Если вы отлаживаетесь и хотите сделать Step Over для Enter/LeaveWorkerThread, то вы должны поставить breakpoint сразу после вызова этих функций.
При повторном вхождении в EnterWorkerThread рабочий поток не обязан быть тем же самым, что и при первом вызове. Например:
begin
EnterWorkerThread;
try
finally
LeaveWorkerThread;
end;
EnterWorkerThread;
try
finally
LeaveWorkerThread;
end;
end;
|
|
Для временного переключения в главный поток используйте функции из AsyncCalls EnterMainThread/LeaveMainThread. Для них справедливы все те же замечания, что и для EnterWorkerThread/LeaveWorkerThread. Например:
begin
EnterWorkerThread;
try
EnterMainThread;
try
finally
LeaveMainThread;
end;
finally
LeaveWorkerThread;
end;
end;
|
|
Вызов EnterMainThread/LeaveMainThread подобен вызову Synchronize. Поскольку одновременно в главном потоке может выполняться лишь один код, то EnterMainThread блокирует (вызовом EnterCriticalSection) выполнение потока, если уже есть поток, вызвавший EnterMainThread и не вызвавший ещё LeaveMainThread. Также, во время вызова EnterMainThread/LeaveMainThread рабочий поток ожидает завершения работы блока и не возвращается в пул свободных рабочих потоков.
В каких случаях имеет смысл применять такие конструкции?
Преимущество подобного подхода перед обычным (создание менеджера потоков или специализированного TThread, организация очереди задач на выполнение и т.п.) в том, что не нужно делать совершенно никаких усилий для организации многопоточной работы. Конечно, предложенный подход не достаточно гибок для серьёзных задач. Но намного чаще встречаются задачки, когда нужно просто выполнить что-то длительное, не подвешивая GUI, не заботясь о том, как и что там будет крутиться. Сколько таких примеров можно встретить в программах. Одна программа виснет на время поиска файла, т.к. автор не думал, что плагинов будет больше 10, а у вас их штук пятьсот. Другая виснет при чтении файла в память, т.к. автор плюнул на потоки из-за сложности реализации и оформления кода ("это ж целый модуль нужно создать! А что тут писать в Execute? Ой, а как параметры передать?" — воскликнет он).
Вот на помощь и приходит этот модуль. Он предназначен сугубо для решения этой задачи. Чтобы сделать приложение "гладким", нужно выделить потенциально длительные места и обернуть их в Enter/LeaveWorkerThread. Тут нет ни приоритетов, ни Terminate, ни Queue. Он предназначен для "прозрачного" программирования. С ним можно писать код, не заботясь о потоках вообще. Просто нужно выделить блоки кода типа "может занять много времени" и "требует обращения к VCL" и обрамить их в вызовы EnterXXXThread/LeaveXXXThread. Всё! Никакой передачи параметров, синхронизации и т.п.
Также один из сценариев — модификация уже существующего кода. Например, написана большая процедура. Позднее обнаружилось, что одну её часть желательно выполнять в дополнительном потоке. Вместо того чтобы разгребать код, рассортировывать переменные, организовывать обмен данными, достаточно просто вставить EnterWorkerThread/LeaveWorkerThread вокруг проблемного участка.
Окей, в конце концов, есть и готовые решения на эту тему: взять хоть тот же AsyncCalls. Но они требуют разбиения моего "красивейшего куска кода" на уродливые процедуры в самых неподходящих местах :) Ладно, может я и прикалываюсь, но есть некая ценность в инструменте, позволяющем написать одну ясно читаемую процедуру так, что одна её часть выполняется в главном потоке, а другая — в дополнительном.
Короче говоря, этот модуль — удобная альтернатива стандартным потокам для случаев, когда не хочется заниматься вознёй с потоками.
Для работы модуля требуется модуль AsyncCalls (версии 2.0 или выше) от Andreas Hausladen, который можно взять тут. Фактически, из него берётся только одна процедура, а именно — LocalAsyncCallEx, для планирования выполнения процедуры менеджером потоков. Если у вас есть другая, предпочитаемая вами, реализация пула потоков, то модуль TasksEx можно легко исправить, чтобы он использовал другую библиотеку потоков.
Также вместе с модулем поставляется плагин для Delphi Language Extensions версии не ниже, чем от 2007-06-18. Для установки плагина нужно создать папку DLangExtPlugins (если её нет) в той папке, где у вас стоят Delphi Language Extensions (обычно это папка bin Delphi), и скопировать файл TasksExPlugin.dll в папку DLangExtPlugins.
Плагин производит простейшую предварительную обработку кода, заменяя следующие строки:
begin(thread) -> EnterWorkerThread; try
begin(main) -> EnterMainThread; try
end(thread) -> finally LeaveWorkerThread; end;
end(main) -> finally LeaveMainThread; end;
|
|
После закрывающей скобки ")" опционально может стоять точка с запятой ";". А перед открывающей скобкой "(" может опционально стоять один пробел " " (но не более).
С использованием этого плагина простейший пример использования функций модуля будет выглядеть так:
procedure TForm1.Button1Click(Sender: TObject);
var
X: Integer;
begin
begin(thread)
for X := 0 to 99 do
begin
Sleep(100);
begin(main)
ProgressBar1.StepIt;
end(main);
Sleep(100);
end;
end(thread);
ShowMessage('Операция выполнена.');
end;
|
|
Плагин не добавляет никакой новой функциональности, а просто позволяет иначе записывать исходный код.
В качестве альтернативы можно рассмотреть и такой вариант (inc-файлы входят в архив с модулем и примерами):
procedure TForm1.Button1Click(Sender: TObject);
var
X: Integer;
begin
for X := 0 to 99 do
begin
Sleep(100);
ProgressBar1.StepIt;
Sleep(100);
end;
ShowMessage('Операция выполнена.');
end;
|
|
Примечание:
- На момент написания статьи модуль AsyncCalls не поддерживал использование EnterMainThread/LeaveMainThread в программах, использующих run-time пакеты. Процедуры EnterWorkerThread/LeaveWorkerThread не имеют такого ограничения.
- В модуле TasksEx.pas EnterWorkerThread/LeaveWorkerThread объявлены как оперирующие с переменной типа TContext. Это сделано с целью совместимости со старой реализацией. Новая реализация не использует TContext.
- Для желающих перекомпилировать плагин для Delphi Language Extensions: объявите в свойствах проекта переменную "TASKSEXPLUGIN" (Project/Options/Directories/Conditionals/Conditional defines). Вам также понадобится интерфейсный модуль LangExtIntf.pas. Взять его можно там же (я нашёл его по ссылке "Example plugin").
К материалу прилагаются файлы:
[Взаимодействия между потоками приложения]
Обсуждение материала [ 23-12-2011 01:02 ] 41 сообщение |