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

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


Вашій увазі (читай: для використання і тестування) пропонується модуль 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/

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*