ShowMessage (IntToStr(ThreadListl. Count));
і навіть не
MainForm. SyncShowMessage;
а тільки так:
Synchronize (MainForm. SyncShowMessage);
Примітка
Виробляючи будь-яке звернення до об'єкту VCL з потоку, переконайтеся, що при цьому використовується метод Synchronize; інакше результати можуть виявитися непередбачуваними. Це вірно навіть в тому випадку, якщо ви використовуєте засоби синхронізації, описані нижче.
procedure Resume;
Метод Resume класу TThread викликається, коли потік відновлює виконання після зупинки, або для явного запуску потоку, створеного з параметром CreateSuspended, рівним True.
procedure Suspend;
Виклик методу Suspend припиняє потік з можливістю повторного запуску згодом. Метод suspend припиняє потік незалежно від коду, виконуваного потоком в даний момент; виконання продовжується з точки останову.
property Suspended: Boolean;
Властивість suspended дозволяє програмісту визначити, чи не припинений потік. За допомогою цієї властивості можна також запускати і зупиняти потік. Встановивши властивість suspended в значення True, ви одержите той же результат, що і при виклику методу Suspend – припинення. Навпаки, установка властивості Suspended в значення False відновлює виконання потоку, як і виклик методу Resume.
property ReturnValue: Integer;
Властивість ReturnValue дозволяє взнати і встановити значення, що повертається потоком після його завершення. Ця величина повністю визначається користувачем. За умовчанням потік повертає нуль, але якщо програміст захоче повернути іншу величину, то просте встановлення заново властивості ReturnValue усередині потоку дозволить одержати цю інформацію іншим потокам. Це, наприклад, може стати в нагоді, якщо усередині потоку виникли проблеми, або за допомогою властивості ReturnValue потрібно повернути число не минулих орфографічну перевірку слів.
На цьому завершимо докладний огляд класу TThread. Для ближчого знайомства з потоками і класом Delphi TThread створимо багатопотоковий додаток. Для цього потрібно написати всього декілька рядків коду і кілька разів клацнути мишею.
Засоби синхронізації потоків
Простіше всього говорити про синхронізацію, якщо створюваний потік не взаємодіє з ресурсами інших потоків і не звертається до VCL. Припустимо, у вас на комп'ютері декілька процесорів, і ви хочете «розпаралелювати» обчислення. Тоді цілком доречний наступний код:
MyCompThread:= TComputationThread. Create(False);
// Тут можна що-небудь робити, поки другий потік виробляє обчислення
DoSomeWork;
// Тепер чекаємо його завершення
MyCompThread. WaitFor;
Приведена схема абсолютно недопустима, якщо під час своєї роботи потік MyCompThread звертається до VCL за допомогою методу synchronize. В цьому випадку потік чекає головного потоку для звернення до VCL, а той, у свою чергу, його – класична безвихідь.
За «порятунком» слід звернутися до програмного інтерфейсу Win32. Він надає багатий набір інструментів, які можуть знадобитися для організації спільної роботи потоків.
Головні поняття для розуміння механізмів синхронізації – функції очікування і об'єкти синхронізації. У Windows API передбачений ряд функцій, що дозволяють припинити виконання потоку, що викликав цю функцію, аж до того моменту, як буде змінений стан якогось об'єкту, званого об'єктом синхронізації (під цим терміном тут розуміється не об'єкт Delphi, а об'єкт операційної системи). Проста з цих функцій – waitForSingieCbject – призначена для очікування одного об'єкту.
До можливих варіантів відносяться чотири об'єкти, які розроблені спеціально для синхронізації: подія (event), взаємне виключення (mutex), семафор (semaphore) і таймер (timer).
Але окрім спеціальних об'єктів можна організувати очікування і інших об'єктів, дескриптор яких використовується в основному для інших цілей, але може застосовуватися і для очікування. До них відносяться: процес (process), потік (thread), сповіщення про зміну у файловій системі (change notification) і консольне введення (console input).
Побічно до цієї групи може бути додана критична секція (critical section).
Примітка
Перераховані вище засоби синхронізації в основному інкапсульовані до складу класів Delphi. У програміста є дві альтернативи. З одного боку, до складу бібліотеки VCL включений модуль SYNCOBJS.PAS, що містить класи для події (TEvent) і критичної секції (TCriticalSection). З іншою, з Delphi поставляється відмінний приклад IPCDEMOS, який ілюструє проблеми взаємодії процесів і містить модуль IPCTHRD.PAS з аналогічними класами – для тієї ж події, взаємного виключення (TMutex), а також спільно використовуваної пам'яті (TSharedMem).
Перейдемо до докладного опису об'єктів, використовуваних для синхронізації.
Подія
Об'єкт типу подія (event) – простий вибір для задач синхронізації. Він подібний дверному дзвінку – дзвенить до тих пір, поки його кнопка знаходиться в натиснутому стані, сповіщаючи про цей факт оточуючих. Аналогічно, і об'єкт може бути в двох станах, а «чути» його можуть багато потоків відразу. Клас TEvent (модуль SYNCOBJS.PAS) має два методи: setEvent і ResetEvent, які переводять об'єкт в активний і пасивний стан відповідно. Конструктор має наступний вигляд:
constructor Create (EventAttributes: PSecurityAttributes;
ManualReset, InitialState: Boolean; const Name: string);
Тут параметр initialstate – початковий стан об'єкту, ManualReset – спосіб його скидання (перекладу в пасивний стан). Якщо цей параметр рівний True, подія повинна бути скинуте уручну. Інакше подія скидається у міру того, як стартує хоч один потік, що чекав даного об'єкту.
На третьому методі:
TWaitResult = (wrSignaled, wrTimeout, wrAbandoned, wrError);
function WaitFor (Timeout: DWORD): TWaitResult;
зупинимося докладніше. Він дає можливість чекати активізації події протягом Timeout мілісекунд. Як ви могли здогадатися, усередині цього методу відбувається виклик функції waitFotsingieObject. Типових результатів на виході waitFor два – wrsignaied, якщо відбулася активізація події, і wrTimeout, якщо за час тайм-ауту нічого не відбулося.
Примітка
Якщо потрібно (і допустимо!) чекати нескінченно довго, слід встановити параметр Timeout в значення INFINITE.
Розглянемо маленький приклад. Включимо до складу нового проекту об'єкт типа TThread, наповнивши його метод Execute наступним вмістом:
Var res: TWaitResult;
procedure TSimpleThread. Execute;
begin
e:= TEvent. Create (nil, True, false, 'test');
repeat
e. ReSetEvent;
res:= e. WaitFor(10000);
Synchronize(Showlnfo);
until Terminated; e. Free;
end;
procedure TSimpleThread. Showlnfo;
begin
ShowMessage (IntToStr(Integer (res)));
end;
На головній формі розмістимо дві кнопки – натиснення однієї з них запускає потік, натиснення другої активізує подію:
procedure TForml. ButtonlClick (Sender: TObject);
begin
TSimpleThread. Create(False);
end;
procedure TForml. Button2Click (Sender: TObject);
begin
e. SetEvent;
end;
Натиснемо першу кнопку. Результат (метод Showlnfo), що тоді з'явився на екрані, залежатиме від того, чи була натиснута друга кнопка або закінчилися відведені 10 секунд.
Події використовуються не тільки для роботи з потоками – деякі процедури операційної системи автоматично перемикають їх. До числа
таких процедур відносяться відкладене (overlapped) введення / висновок і події, пов'язані з комунікаційними портами.
Взаємні виключення
Об'єкт типу взаємне виключення (mutex) дозволяє тільки одному потоку зараз володіти ним. Якщо продовжувати аналогії, то цей об'єкт можна порівняти з естафетною паличкою.
Клас, що інкапсулює взаємне виключення, – TMutex – знаходиться в модулі IPCTHRD.PAS (приклад IPCDEMOS). Конструктор:
constructor Create (const Name: string);
задає ім'я створюваного об'єкту. Спочатку він не належить нікому. (Але функція API createMutex, що викликається в ньому, дозволяє передати створений об'єкт тому потоку, в якому це відбулося.) Далі метод
function Get (TimeOut: Integer): Boolean;
виробляє спробу протягом Timeout мілісекунд заволодіти об'єктом (в цьому випадку результат рівний True). Якщо об'єкт більш не потрібен, слід викликати метод
function Release: Boolean;
Програміст може використовувати взаємне виключення, щоб уникнути прочитування і запису загальної пам'яті декількома потоками одночасно.
Семафор
Семафор (semaphore) подібний взаємному виключенню. Різниця між ними у тому, що семафор може управляти кількістю потоків, які мають до нього доступ. Семафор встановлюється на граничне число потоків, яким доступ дозволений. Коли це число досягнуте, подальші потоки будуть припинені, поки один або більш потоків не від'єднаються від семафора і не звільнять доступ.
Як приклад використовування семафора розглянемо випадок, коли кожний з групи потоків працює з фрагментом спільно використовуваного пулу пам'яті. Оскільки спільно використовувана пам'ять допускає звернення до неї тільки певного числа потоків, всі інші повинні бути блоковані аж до моменту, коли один або декілька користувачів пулу відмовляться від його сумісного використовування.
Критична секція
Працюючи в Delphi, програміст може також використовувати об'єкт типу критична секція (critical section). Критичні секції подібні взаємним виключенням по суті, проте між ними існують дві головні відмінності:
· взаємні виключення можуть бути спільно використані потоками в різних процесах, а критичні секції – ні;
· якщо критична секція належить іншому потоку, чекаючий потік блокується аж до звільнення критичної секції. На відміну від цього, взаємне виключення дозволяє продовження після закінчення тайм-ауту.
Критичні секції і взаємні виключення дуже схожі. На перший погляд, виграш від використовування критичної секції замість взаємного виключення не очевидний. Критичні секції, проте, більш ефективні, ніж взаємні виключення, оскільки використовують менше системних ресурсів. Взаємні виключення можуть бути встановлені на певний інтервал часу, після закінчення якого виконання продовжується; критична секція завжди чекає стільки, скільки потрібно.
Візьмемо клас TCriticalSection (модуль SYNCOBJS.PAS). Логіка використовування його проста – «тримати і не пущать». У багатопотоковому додатку створюється і ініціалізується загальна для всіх потоків критична секція. Коли один з потоків досягає критично важливої ділянки коду, він намагається захопити секцію викликом методу Enter:
MySection. Enter; try DoSomethingCritical;
finally
MySection. Leave;
end;
Коли інші потоки доходять до оператора захоплення секції Enter і знаходять, що вона вже захоплена, вони припиняються аж до звільнення секції першим потоком шляхом виклику методу Leave. Зверніть увагу, що виклик Leave поміщений в конструкцію try..finally – тут потрібна стовідсоткова надійність. Критичні секції є системними об'єктами і підлягають обов'язковому звільненню – втім, як і решта об'єктів, що розглядаються тут.