Rambler's Top100
"Knowledge itself is power"
F.Bacon
Поиск | Карта сайта | Помощь | О проекте | ТТХ  
 Подземелье Магов
  
 

Фильтр по датам

 
 К н и г и
 
Книжная полка
 
 
Библиотека
 
  
  
 


Поиск
 
Поиск по КС
Поиск в статьях
Яndex© + Google©
Поиск книг

 
  
Тематический каталог
Все манускрипты

 
  
Карта VCL
ОШИБКИ
Сообщения системы

 
Форумы
 
Круглый стол
Новые вопросы

 
  
Базарная площадь
Городская площадь

 
   
С Л С

 
Летопись
 
Королевские Хроники
Рыцарский Зал
Глас народа!

 
  
ТТХ
Конкурсы
Королевская клюква

 
Разделы
 
Hello, World!
Лицей

Квинтана

 
  
Сокровищница
Подземелье Магов
Подводные камни
Свитки

 
  
Школа ОБЕРОНА

 
  
Арсенальная башня
Фолианты
Полигон

 
  
Книга Песка
Дальние земли

 
  
АРХИВЫ

 
 

Сейчас на сайте присутствуют:
 
  
 
Во Флориде и в Королевстве сейчас  15:59[Войти] | [Зарегистрироваться]

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

Александр Алексеев
дата публикации 06-06-2008 05:46

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

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

procedure EnterWorkerThread;
procedure LeaveWorkerThread;

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

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

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

begin

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

  EnterWorkerThread;
  try

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

  finally
    LeaveWorkerThread;
  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:

...

EnterWorkerThread;
try
  ...
  P;
  ...
finally
  LeaveWorkerThread;
end;

...

procedure P;
begin
  EnterWorkerThread; // ничего не делает,
                     // т.к. мы уже во вторичном потоке
  try
    ...
  finally
    LeaveWorkerThread; // ничего не делает, т.к. EnterWorkerThread
                       // ничего не делал
  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

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

  finally
    LeaveWorkerThread;
  end;

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

  EnterWorkerThread;
  try

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

  finally
    LeaveWorkerThread;
  end;

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

end;

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

begin

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

  EnterWorkerThread;
  try

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

    EnterMainThread;
    try

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

    finally
      LeaveMainThread;
    end;

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

  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)
        // для оперирования с VCL мы должны быть в главном потоке    
        ProgressBar1.StepIt;      
      end(main);
      Sleep(100); // ...ещё что-то делаем
    end;
  end(thread);
  ShowMessage('Операция выполнена.');
end;

Плагин не добавляет никакой новой функциональности, а просто позволяет иначе записывать исходный код.

В качестве альтернативы можно рассмотреть и такой вариант (inc-файлы входят в архив с модулем и примерами):

procedure TForm1.Button1Click(Sender: TObject);
var
  X: Integer;
begin
  {$I BeginThread.inc}
    // этот цикл выполняется во вторичном рабочем потоке
    for X := 0 to 99 do
    begin
      Sleep(100); // что-то делаем...
      // настало время обновить процент выполненной работы
      {$I BeginMain.inc}
        // для оперирования с VCL мы должны быть в главном потоке    
        ProgressBar1.StepIt;      
      {$I EndMain.inc}
      Sleep(100); // ...ещё что-то делаем
    end;
  {$I EndThread.inc}
  ShowMessage('Операция выполнена.');
end;

Примечание:

  1. На момент написания статьи модуль AsyncCalls не поддерживал использование EnterMainThread/LeaveMainThread в программах, использующих run-time пакеты. Процедуры EnterWorkerThread/LeaveWorkerThread не имеют такого ограничения.
  2. В модуле TasksEx.pas EnterWorkerThread/LeaveWorkerThread объявлены как оперирующие с переменной типа TContext. Это сделано с целью совместимости со старой реализацией. Новая реализация не использует TContext.
  3. Для желающих перекомпилировать плагин для Delphi Language Extensions: объявите в свойствах проекта переменную "TASKSEXPLUGIN" (Project/Options/Directories/Conditionals/Conditional defines). Вам также понадобится интерфейсный модуль LangExtIntf.pas. Взять его можно там же (я нашёл его по ссылке "Example plugin").


К материалу прилагаются файлы:


Смотрите также материалы по темам:
[Взаимодействия между потоками приложения]

 Обсуждение материала [ 23-12-2011 01:02 ] 41 сообщение
  
Время на сайте: GMT минус 5 часов

Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter.
Функция может не работать в некоторых версиях броузеров.

Web hosting for this web site provided by DotNetPark (ASP.NET, SharePoint, MS SQL hosting)  
Software for IIS, Hyper-V, MS SQL. Tools for Windows server administrators. Server migration utilities  

 
© При использовании любых материалов «Королевства Delphi» необходимо указывать источник информации. Перепечатка авторских статей возможна только при согласии всех авторов и администрации сайта.
Все используемые на сайте торговые марки являются собственностью их производителей.

Яндекс цитирования