Робота з "потоками" в середовищі Delphi

Робота з «потоками» в середовищі Delphi






Працюючи з Delphi, потрібно мати на увазі: цей чудовий продукт не тільки спрощує розробку складних додатків, він використовує при цьому всі можливості операційної системи. Одна з можливостей, яку підтримує Delphi, – це так звані потоки (threads) або нитки.

Потоки дозволяють в рамках однієї програми вирішувати декілька задач одночасно. З недавніх пір операційні системи для персональних комп'ютерів зробили це можливим.

Операційна система (ОС) надає додатку деякий інтервал часу центрального процесора (ЦП) і в мить, коли додаток переходить до очікування повідомлень або звільняє процесор, операційна система передає управління іншій задачі. Тепер, коли комп'ютери з більш ніж одним процесором різко впали в ціні, а операційна система Windows NT може використовувати наявність декількох процесорів, користувачі дійсно можуть запускати одночасно більше однієї задачі. Плануючи час центрального процесора, Windows 95 або Windows NT розподіляють його між потоками, а не між додатками. Щоб використовувати всі переваги, забезпечувані декількома процесорами в сучасних операційних системах, програміст повинен знати, як створювати потоки.

У цьому рефераті розглядаються наступні питання:

що таке потоки;

різниця між потоком і процесом;

переваги потоків;

клас TThread в Delphi;

	реалізація багатопотокового додатку;

	синхронізація потоків.

Визначення потоку досить просте: потоки – це об'єкти, одержуючі час процесора. Час процесора виділяється квантами (quantum, time slice). Квант часу – це інтервал, що є у розпорядженні потоку доти. поки час не буде передано в розпорядження іншого потоку.

Кванти виділяються не програмам або процесам, а породженим ними потокам. Як мінімум, кожен процес має хоча б один (головний) потік, але сучасні операційні системи, починаючи з Windows 95 (для прихильників Borland Kylix і Linux також), дозволяють запустити в рамках процесу декілька потоків.

Найпростіший приклад їх використовування – додатки з складу Microsoft Office. Наприклад, пакети Excel і Word задіють по декілька потоків. Word може одночасно коректувати граматику і друкувати, при цьому здійснюючи введення даних з клавіатури і миші; програма Excel здатна виконувати фонові обчислення і друкувати.

Примітка

Взнати число потоків, запущених додатком, в Windows NT, 2000 і ХР можна за допомогою утиліти Task Manager (Диспетчер задач). Для цього серед показників, що відображаються у вікні Processes, потрібно вибрати опцію Thread Count. Так, у момент написання цих рядків MS Word використовував 5 потоків, середовище Delphi – 3.

Якщо задачі додатку можна розділити на різні підмножини: обробка подій, введення / висновок, зв'язок і ін., то потоки можуть бути органічно вбудовані в програмне рішення. Якщо розробник може розділити велику задачу на декілька дрібних, це тільки підвищить переносимість коду і можливості його багатократного використовування.

Зробивши додаток багатопотоковим, програміст дістає додаткові можливості управління їм. Наприклад, через управління пріоритетами потоків. Якщо один з них «пригальмовує» додаток, займаючи дуже багато процесорний час, його пріоритет може бути знижений.

Інша важлива перевага упровадження потоків – при зростанні «навантаження» на додаток можна збільшити кількість потоків і тим самим зняти проблему.

Потоки спрощують життя тим програмістам, які розробляють додатки в архітектурі клієнт/сервер. Коли потрібне обслуговування нового клієнта, сервер може запустити спеціально для цього окремий потік. Такі потоки прийнято називати симетричними потоками (symmetric threads) – вони мають однакове призначення, виконують один і той же код і можуть розділяти одні і ті ж ресурси. Більш того, додатки, розраховані на серйозне навантаження, можуть підтримувати пул (pool) однотипних потоків. Оскільки створення потоку вимагає певного часу, для прискорення роботи бажано наперед мати потрібне число готових потоків і активізувати їх у міру підключення чергового клієнта.

Примітка

Такий підхід особливо характерний для Webсерверу Microsoft Internet Information Services і додатків, оброблювальних запити в його середовищі. Якщо ви створюєте додатки ISAPI на Delphi, то можете використовувати пулінг потоків, підключивши до проекту модуль ISAPIThreadPool.pas. Якщо ви хочете запозичити ідеї для інших цілей, ознайомтеся з вмістом цього модуля.

Асиметричні потоки (asymmetric threads) – це потоки, вирішальні різні задачі і, як правило, не розділяючі сумісні ресурси. Необхідність в асиметричних потоках виникає:

коли в програмі необхідні тривалі обчислення, при цьому необхідно зберегти нормальну реакцію на введення;

коли потрібно обробляти асинхронне введення / висновок з використанням різних пристроїв (Сом-порту, звукової карти, принтера і т. п.);

коли ви хочете створити декілька вікон і одночасно обробляти введення в них.

Коли ми говоримо «програма» (application), то звичайно маємо на увазі поняття, в термінології операційної системи що позначається як «процес». Процес складається з віртуальної пам'яті, виконуваного коду, потоків і даних. Процес може містити багато потоків, але обов'язково містить, принаймні, один. Потік, як правило, має «у власності» мінімум ресурсів; він залежить від процесу, який і розпоряджається віртуальною пам'яттю, кодом, даними, файлами і іншими ресурсами ОС.

Чому ми використовуємо потоки замість процесів, хоча, при необхідності, додаток може складатися і з декількох процесів? Річ у тому, що перемикання між процесами – значно більш трудомістка операція, ніж перемикання між потоками. Інший довід на користь використовування потоків – те, що вони спеціально задумані для розділення ресурсів; розділити ресурси між процесами (що мають роздільний адресний простір) не так-то просто.

Тут ми розглянемо можливість для організації фонових дій (job) усередині однопотокової програми із збереженням реакції цього потоку на події від миші і клавіатури.

Ще не так давно програмісти намагалися емулювати потоки, запускаючи процедури усередині циклу обробки повідомлень Windows. Цикл обробки повідомлень (або цикл очікування) – це особливий фрагмент коду в програмі, керованій подіями. Він виконується тоді, коли програма знаходить в черзі події, які потрібно обробити; якщо таких немає, програма може виконати в цей час «фонову процедуру». Такий спосіб імітації потоків вельми складний, оскільки вимушує програміста, по-перше, зберігати стан фонової процедури між її викликами, а по-друге, визначати момент, коли вона поверне управління обробнику подій. Якщо така процедура виконується довго, то у користувача може скластися враження, що додаток перестав реагувати на зовнішні події. Використовування потоків знімає проблему перемикання контексту, тепер контекст (стек і регістри) зберігає операційна система.

У Delphi можливість створити фонову процедуру реалізована через подію Onldle.объекта Application!

type TIdleEvent = procedure (Sender: TObject;

var Done: Boolean)

of object;

property Onldle: TIdleEvent;

Обробник цієї події ви можете написати, помістивши на форму компонент TApplicationEvents із сторінки Additional Палітри компонентів.

Щоб зробити у фоновому режимі якусь роботу, слід розбити її на кванти і виконувати по одному кванту кожен виклик Onldle – інакше додаток погано реагуватиме на зовнішні дії.

Інтерфейс Win32 API дозволяє програмісту управляти розподілом часу між потоками; це розповсюджується і на додатки, написані на Delphi. Операційна система планує час процесора відповідно до пріоритетів потоків.

Пріоритет потоку – величина, що складається з двох складових частин: пріоритету породжувача потік процесу і власне пріоритету потоку. Коли потік створюється, йому призначається пріоритет, відповідний пріоритету його процесу, що породив.

У свою чергу, процеси можуть мати наступні класи пріоритетів.

Real time;

Normal;

	High;

Below normal;

Above normal;

	Idle.

Примітка

Класи Above normal і Below normal з'явилися вперше в Windows 2000.

Клас реального часу задає пріоритет навіть більший, ніж у багатьох процесів операційної системи. Такий пріоритет потрібен для процесів, оброблювальних високошвидкісні потоки даних. Якщо такий процес не завершиться за короткий час, користувач відчує, що система перестала відгукуватися, оскільки навіть обробка подій миші не одержить часу процесора.

Використовування класу High обмежене процесами, які повинні завершуватися за короткий час, щоб не викликати збійної ситуації. Приклад – процес, який посилає сигнали зовнішньому пристрою; причому пристрій відключається, якщо не одержить своєчасний сигнал. Якщо у вас виникли проблеми з продуктивністю вашого додатку, було б неправильно вирішувати їх просто за рахунок підвищення його пріоритету до high – такий процес також впливає на всю ОС. Можливо, в цьому випадку слід модернізувати комп'ютер.

Більшість процесів запускається в рамках класу з нормальним пріоритетом. Нормальний пріоритет означає, що процес не вимагає якої-небудь спеціальної уваги з боку операційної системи.

І нарешті, процеси з фоновим пріоритетом запускаються лише в тому випадку, якщо в черзі Диспетчера задач немає інших процесів. Звичні види додатків, використовуючи такий пріоритет, – це програми збереження екрану і системні агенти (system agents). Програмісти можуть використовувати фонові процеси для організації завершальних операцій і реорганізації даних. Прикладами можуть служити збереження документа або резервне копіювання бази даних.

Пріоритети мають значення від 0 до 31. Процес, що породив потік, може згодом змінити його пріоритет; у цій ситуації програміст має нагоду управляти швидкістю відгуку кожного потоку.

Базовий пріоритет нитки складається з двох складових, проте це не означає, що він просто рівний їх сумі. Погляньте на відповідні величини, які показані в табл. 29.1. Для потоку, що має власний пріоритет THREAD_PRIORITY_IDLE, базовий пріоритет буде рівний 1, незважаючи на пріоритет його процесу, що породив.

І ще для класу Normal приведені по два пріоритети, забезпечені буквами В (Background) і F (Foreground). Пояснення цьому дається нижче.




Таблиця 29.1. Класи процесів і пріоритети їх потоків (для Windows 2000 і ХР)



IDLE_ PRIORITY CLASS

BELOW_ NORMAL PRIORITY CLASS

NORMAL_ PRIORITY_ CLASS

ABOVE_ NORMAL_ PRIORITY_ CLASS

HIGH PRIORITY CLASS

REALTIME PRIORITY CLASS

THREAD_ PRIORITY_

IDLE

1

1

1

1

1

16

THREAD_ PRIORITY LOWEST

2

4

5 (B)

7 (F)

8

11

22

THREAD_ PRIORITY_ BELOW NORMAL

3

5

6 (B)

8 (F)

9

12

23

THREAD_ PRIORITY_ NORMAL

4

6

7 (B)

9 (F)

10

13

24

THREAD PRIORITY_ ABOVE_ NORMAL

5

7

8 (В)

10 (F)

11

14

25

THREAD_ PRIORITY_ HIGHEST

6

8

9 (B)

11 (F)

12

15

26

THREAD_ PRIORITY TIME CRITICAL

15

15

15

15

15

31



Крім базового пріоритету, описуваного в цій таблиці, планувальник завдань (scheduler) може призначати так звані динамічні пріоритети. Для процесів класу NORMAL_PRIORITY_CLASS при перемиканні з фонового режиму в режим переднього плану і у ряді інших випадків пріоритет потоку, з яким створено вікно переднього плану, підвищується. Так працюють всі клієнтські операційні системи від Microsoft. Серверні операційні системи оптимізовані для виконання фонових додатків. Втім, Windows NT і пізніші ОС на цьому ядрі дозволяють перемикати режим оптимізації, використовуючи перемикач Application response аплета System панелі управління Windows (мал. 29.1).

До того ж Windows 2000 Professional і Windows 2000 Server мають різні алгоритми виділення квантів часу. Перша – клієнтська – операційна система виділяє час короткими квантами змінної довжини для прискорення реакції на додатки переднього плану (foreground). Для серверу ж більш важлива стабільна робота системних служб, тому в другій ОС система розподіляє довгі кванти постійної довжини.





Мал. 29.1. За допомогою діалогу Performance Options можна управляти алгоритмом призначення пріоритетів



Тепер, розібравшись в пріоритетах потоків, потрібно обов'язково сказати про те, як же їх використовує планувальник завдань для розподілу процесорного часу.

Операційна система має різні черги готових до виконання потоків – для кожного рівня пріоритету свій. У момент розподілу нового кванта часу вона проглядає черги – від вищого пріоритету до нижчого. Готовий до виконання потік, що стоїть першим в черзі, одержує цей квант і переміщається в хвіст черги. Потік виконуватиметься всю тривалість кванта, якщо не відбудеться одна з двох подій:

потік, що виконується, зупинився для очікування;

	з'явився готовий до виконання потік з вищим пріоритетом.

Тепер, напевно, вам більш ясна небезпека, витікаюча від невиправданого завищення пріоритетів. Адже, якщо є активні потоки з високим пріоритетом, жоден потік з нижчим пріоритетом жодного разу не одержить часу процесора. Ця проблема може підстерігати вас навіть на рівні вашого додатку. Припустимо, ви призначили обчислювальному потоку пріоритет THREAD_PRIORITY_ABOVE_NORMAL, а потоку, де обробляється введення користувача, – THREAD_PRIORITY_BELOW_NORMAL. Тоді замість запланованого результату – сумістити обчислення з нормальною реакцією додатку – ви одержите строго зворотний. Додаток взагалі перестане відгукуватися на введення, і зняти його буде можливо тільки за допомогою засобів ОС.

Отже нормальна практика для асиметричних потоків – це призначення потоку, оброблювальному введення, вищого пріоритету, а всім іншим – нижчого або навіть пріоритету idle, якщо цей потік повинен виконуватися тільки під час простою системи.

Клас TThread

Delphi представляє програмісту повний доступ до можливостей програмування інтерфейсу Win32. Для чого ж тоді фірма Borland представила спеціальний клас для організації потоків? Взагалі кажучи, програміст не зобов'язаний розбиратися у всій тонкості механізмів, пропонованих операційною системою. Клас повинен інкапсулювати і спрощувати програмний інтерфейс; клас TThread – прекрасний приклад надання розробнику простого доступу до програмування потоків. Сам API потоків, взагалі кажучи, не дуже складний, але надані класом TThread можливості взагалі чудово прості. Двома словами, все, що вам необхідно зробити, – це перекрити віртуальний метод Execute.

Інша відмінна риса класу TThread – це гарантія безпечної роботи з бібліотекою візуальних компонентів VCL. Без використовування класу TThread під час викликів VCL можуть виникнути ситуації, що вимагають спеціальної синхронізації (див. разд. «Проблеми при синхронізації потоків» далі в цьому розділі).

Потрібно віддавати собі звіт, що з погляду операційної системи потік – це її об'єкт. При створенні він одержує дескриптор і відстежується ОС. Об'єкт класу TThread – це конструкція Delphi, відповідна потоку ОС. Цей об'єкт VCL створюється до реального виникнення потоку в системі і знищується після його зникнення.

Вивчення класу TThread почнемо з методу Execute:

procedure Execute; virtual; abstract;

Це і є код, виконуваний в створюваному вами потоці TThread.

Примітка

Хоча формальний опис Execute – метод abstract, але майстер створення нового об'єкту TThread створює для вас порожній шаблон цього методу.

Метод Execute, ми можемо тим самим закладати в новий потоковий клас те, що виконуватиметься при його запуску. Якщо потік був створений з аргументом CreateSuspended, рівним False, то метод Execute виконується негайно, інакше Execute виконується після виклику методу Resume (див. опис конструктора нижче).

Якщо потік розрахований на одноразове виконання яких-небудь дій, то ніякого спеціального коду завершення усередині Execute писати не треба.

Якщо ж в потоці виконуватиметься якийсь цикл, і потік повинен завершитися разом з додатком, то умови закінчення циклу повинні бути приблизно такими:

procedure TMyThread. Execute;

begin

repeat

DoSomething;

Until CancelCondition or Terminated;

end;

Тут CancelCondition – ваша особиста умова завершення потоку (вичерпання даних, закінчення обчислень, надходження на вхід того або іншого символу і т.п.), а властивість Terminated повідомляє про завершення потоку (ця властивість може бути встановлене як зсередини потоку, так і ззовні; швидше за все, завершується його процес, що породив).

Конструктор об'єкту:

constructor Create (CreateSuspended: Boolean);

одержує параметр CreateSuspended. Якщо його значення рівне True, знов створений потік не починає виконуватися до тих пір, поки не буде зроблений виклик методу Resume. У випадку, якщо параметр CreateSuspended має значення False, конструктор завершується і тільки тоді потік починає виконання.

destructor Destroy; override;

Деструкція Destroy викликається, коли необхідність в створеному потоці відпадає. Деструкція завершує його і вивільняє всі ресурси, пов'язані з об'єктом TThread. function Terminate: Integer;

Для остаточного завершення потоку (без подальшого запуску) існує метод Terminate. Але якщо ви думаєте, що цей метод робить якісь примусові дії по зупинці потоку, ви помиляєтеся. Все, що відбувається, – це установка властивості

property Terminated: Boolean;

у значення True. Таким чином, Terminate – це вказівка потоку завершитися, виражене «в м'якій формі», з можливістю коректно звільнити ресурси. Якщо вам потрібно негайно завершити потік, використовуйте функцію Windows API TerminateThread.

Примітка

Метод Terminate автоматично викликається і з деструкції об'єкту. Поток – об'єкт VCL чекатиме, поки завершиться поток – об'єкт операційної системи. Таким чином, якщо потік не уміє завершуватися коректно, виклик деструкції потенційно може привести до зависання всієї програми.

Ще одна корисна властивість:

property FreeOnTerminate: Boolean;

Якщо це властивість рівне True, то деструкція потоку буде викликана автоматично після його завершення. Це дуже зручно для тих випадків, коли ви в своїй програмі не упевнені точно, коли саме завершиться потік, і хочете використовувати його за принципом «вистрілив і забув» (fire and forget).

function WaitFor: Integer;

Метод WaitFor призначений для синхронізації і дозволяє одному потоку дочекатися моменту, коли завершиться інший потік. Якщо ви усередині потоку FirstThread пишіть код

Code:= SecondThread. WaitFor;

то це означає, що потік FirstThread зупиняється до моменту завершення потоку SecondThread. Метод WaitFor повертає код завершення очікуваного потоку (див. властивість Returnvalue).

property Handle: THandle read FHandle;

property ThreadID: THandle read FThreadID;

Властивості Handle і ThreadID дають програмісту безпосередній доступ до потоку засобами API Win32. Якщо розробник хоче звернутися до потоку і управляти ним, минувши можливості класу TThread, значення Handle і ThreadID можуть бути використані як аргументи функцій Win32 API. Наприклад, якщо програміст хоче перед продовженням виконання додатку дочекатися завершення відразу декількох потоків, він повинен викликати функцію API waitForMuitipieObjects; для її виклику необхідний масив дескрипторів потоків.

property Priority: TThreadPriority;

Властивість Priority дозволяє запитати і встановити пріоритет потоків. Пріоритети потоків в деталях описані вище. Допустимими значеннями пріоритету для об'єктів TThread є tpidle, tpLowest, tpLower, tpNormai, tpHigher, tpHighest і tpTimeCritical.

procedure Synchronize (Method: TThreadMethod);

Цей метод відноситься до секції protected, тобто може бути викликаний тільки з нащадків TThread. Delphi надає програмісту метод Synchronize для

безпечного виклику методів VCL усередині потоків. Щоб уникнути конфліктних ситуацій, метод synchronize дає гарантію, що до кожного об'єкту VCL одночасно має доступ тільки один потік. Аргумент, передаваний в метод Synchronize, – це ім'я методу, який виробляє звернення до VCL; виклик Synchronize з цим параметром – це те ж, що і виклик самого методу. Такий метод (класу TThreadMethod) не повинен мати ніяких параметрів і не повинен повертати ніяких значень. Наприклад, в основній формі додатку потрібно передбачити функцію

procedure TMainForm. SyncShowMessage; begin

ShowMessagedntToStr (ThreadListl. Count)); // інші звернення до VCL

end;

а в потоці для показу повідомлення писати не

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 – тут потрібна стовідсоткова надійність. Критичні секції є системними об'єктами і підлягають обов'язковому звільненню – втім, як і решта об'єктів, що розглядаються тут.

Процес. Породження дочірнього процесу

Об'єкт типу процес (process) може бути використаний для того, щоб припинити виконання потоку в тому випадку, якщо він для свого продовження потребує завершення процесу. З практичної точки зору така проблема встає, коли потрібно в рамках вашого додатку виконати додаток, створений кимось іншим, або, наприклад, сеанс MS-DOS.

Розглянемо, як, власне, один процес може породити інший. Замість застарілої і підтримуваної тільки для сумісності функції winExec, що перекочувала з колишніх версій Windows, набагато правильніше використовувати могутнішу:

function CreateProcess (IpApplicationName: PChar; IpCorranandLine: PChar;

IpProcessAttributes, IpThreadAttributes: PSecurityAttributes;

blnheritHandles: BOOL;

dwCreationFlags: DWORD; IpEnvironment: Pointer;

IpCurrentDirectory: PChar;

const IpStartupInfo: TStartupInfo;

var IpProcessInformation: TProcessInformation): BOOL;

Перші два параметри ясні – це ім'я додатку, що запускається, і передавані йому в командному рядку параметри. Параметр dwCreationFlags містить прапори, що визначають спосіб створення нового процесу і його майбутній пріоритет. Використані в приведеному нижче лістингу прапори означають: CREATE_NEW_CONSOLE – будет запущено новий консольний додаток з окремим вікном; NORMAL_PRIORITY_CLASS – нормальний пріоритет.

Структура TStartupInfo містить відомості про розмір, колір, положення вікна створюваного додатку. У нижченаведеному прикладі (лістинг 29.1) використовується поле wshowwindow: встановлений прапор SW_SHOWNORMAL, що означає візуалізацію вікна з нормальним розміром.

На виході функції заповнюється структура IpProcessInformation. У ній програмісту повертаються дескриптори і ідентифікатори створеного процесу і його первинного потоку. Нам знадобиться дескриптор процесу – в нашому прикладі створюється консольний додаток, потім відбувається очікування його завершення. «Просигналить» нам про це саме об'єкт IpProcessInformation.hProcess.

Лістинг 29.1. Породження дочірнього процесу

var

IpStartupInfo: TStartupInfo;

IpProcessInformation: TProcessInformation;

begin

FillChar (IpStartupInfo, Sizeof(IpStartupInfo), 10);

IpStartupInfo.cb:= Sizeof(IpStartupInfo);

IpStartupInfo.dwFlags:= STARTFJJSESHOWWINDOW; IpStartupInfo.wShowWindow:= SW_SHOWNORMAL;

if not CreateProcess (nil,

PChar ('ping localhost'),

nil,

nil,

false,

CREATE_NEW_CONSOLE or NORMAL_PRIORITY_CLASS,

nil,

nil,

IpStartupInfo, IpProcessInformation) then

ShowMessage (SysErrorMessage(GetLastError;)

else

begin

WaitForSingleObject

(IpProcessInformation.hProcess, 10000); CloseHandle (IpProcessInformation.hProcess);

end;

end;

Потік

Потік може чекати інший потік точно так, як і інший процес. Очікування можна організувати за допомогою функцій API (як в тільки що розглянутому прикладі), але зручніше це зробити за допомогою методу TThread. WaitFor.

Консольне введення

Консольне введення (console input) годиться для потоків, які повинні чекати відгуку на натиснення користувачем клавіші на клавіатурі. Цей тип очікування може бути використаний в програмі дуплексного зв'язку (chat). Один потік при цьому чекатиме отримання символів; другий – відстежувати введення користувача і потім посилати набраний текст чекаючому додатку.

Сповіщення про зміну у файловій системі

Цей вид об'єкту очікування дуже цікавий і незаслужено мало відомий. Ми розглянули практично всі варіанти того, як один потік може подати сигнал іншому. А як одержати сигнал від операційної системи? Ну, наприклад, про те, що у файловій системі відбулися якісь зміни? Такий вид сповіщення з ОС UNIX і доступний програмістам, що працюють з Win32. Для організації моніторингу файлової системи потрібно використовувати

Три функції – FindFirstChangeNotification, FindNextChangeNotification і FinddoseChangeNotification. Перша з них повертає дескриптор об'єкту файлового сповіщення, який можна передати у функцію очікування. Об'єкт активізується тоді, коли в заданій теці відбулися ті або інші зміни (створення або знищення файлу або теки, зміна прав доступу і т. д.). Друга – готує об'єкт до реакції на наступну зміну. Нарешті, за допомогою третьої функції слід закрити той, що став непотрібним об'єкт.

Так може виглядати код методу Execute потоку, створеного для моніторингу файлової системи:

var DirName: string;

…

procedure TSimpleThread. Execute;

var r: Cardinal;

fn: THandle;

begin

fn:= FindFirstChangeNotification (pChar(DirName), True,

FILEJTOTIFY_CHANGE_FILE_NAME);

repeat

r:= WaitForSingleObject (fn, 2000);

if r = WAIT_OBOECT_0 then

Synchronize (Forml. UpdateList);

if not FindNextChangeNotification(fn) then

break;

until Terminated;

FindCloseChangeNotification(fn);

end;

На головній формі повинні знаходитися компоненти, потрібні для вибору обстежуваної теки, а також компонент TListBox, в який записуватимуться імена файлів:

procedure TForml. ButtonlClick (Sender: TObject);

var dir: string; begin

if SelectDirectory (dir, [], 0)

then begin

Editl. Text:= dir; DirName:= dir;

end;

end;

procedure TForml. UpdateList;

var SearchRec: TSearchRec;

begin

ListBoxl. Clear;

FindFirst (Editl. Text+'\*.*', faAnyFile, SearchRec); repeat ListBoxl. Items. Add (SearchRec. Name);

until FindNext(SearchRec)<> 0;

FindClose(SearchRec);

end;

Додаток готовий. Щоб воно стало повнофункціональним, передбачте в ньому механізм перезапуску потоку при зміні обстежуваної теки.

Локальні дані потоку

Цікава проблема виникає, якщо в додатку буде декілька однакових потоків. Як уникнути сумісного використовування одних і тих же змінних декількома потоками? Перше, що спадає на думку, – додати і використати поля об'єкту – нащадка TThread, які можна додати при його створенні. Кожен потік відповідає окремому екземпляру об'єкту, і їх дані перетинатися не будуть. (До речі, це одна з великих зручностей використовування класу TThread.) Але є функції API, які знать не знають про об'єкти Delphi і їх поля і властивості. Для підтримки розділення даних між потоками на нижньому рівні в мову Object Pascal введена спеціальна директива – threadvar, яка відрізняється від директиви опису змінних var тим, що застосовується тільки до локальних даних потоку. Наступний опис:

Var

datal: Integer; threadvar

data2: Integer;

означає, що змінна datal використовуватиметься всіма потоками даного додатку, а змінна data2 буде у кожного потоку своя.

Як уникнути одночасного запуску двох копій одного додатку

Така задача виникає дуже часто. Багато, що особливо починають, користувачів не цілком розуміють, що між клацанням по значку додатку і його запуском може пройти декілька секунд, а то і десятків секунд. Вони починають клацати по значку, запускаючи всі нові копії. Тим часом, при роботі з базами даних і в багатьох інших випадках мати більше однієї копії не тільки не потрібно, але і шкідливо.

Ідея полягає в тому, щоб перша створювана копія додатку захоплювала якийсь, ресурс, а все подальші при запуску намагалися зробити те ж саме і у разі невдачі завершувалися.

Приклад такого ресурсу – загальний блок у файлі, що відображається в пам'ять. Оскільки цей ресурс має ім'я, можна зробити його унікальним саме для вашого додатку:

var UniqueMapping: THandle;

FirstWindow: THandle

; begin

UniqueMapping:= CreateFileMapping ($ffffffff,

nil, PAGE_READONLY, 0, 32,'MyMap');

if UniqueMapping = 0 then

begin

ShowMessage (SysErrorMessage(GetLastError));

Halt;

end

else if GetLastError = ERROR_ALREADY_EXISTS then

begin

FirstWindow:= FindWindowEx (0, 0, TfmMain. ClassName, nil);

if FirstWindowoO then

SetForegroundWindow(FirstWindow);

Halt;

end;

 // Немає інших копій – продовження Application. Initialize;

Приблизно такі рядки потрібно вставити в початок тексту проекту до створення форм. Блок спільно використовуваної пам'яті виділяється в системному сторінковому файлі (про це говорить перший параметр, рівний -1, див. опис функції CreateFileMapping). Його ім'я – муМар. Якщо при створенні блоку буде одержаний код помилки ERROR_ALREADY__EXISTS, це свідчить про наявність працюючої копії додатку. В цьому випадку додаток перемикає фокус на головну форму іншого екземпляра і завершується; інакше процес ініціалізації продовжується.

Потоки, як і інші могутні інструменти, повинні бути використані з обережністю і без зловживань, оскільки можуть виникнути помилки, які дуже важко знайти. Є дуже багато доводів за використовування потоків, але є і доводи проти цього. Робота з потоками буде простішим, якщо враховувати нижче приведені положення.

Якщо потоки працюють тільки із змінними, оголошеними усередині їх власного класу, то ситуації гонок і безвиході украй маловірогідні.

Іншими словами, уникайте використовування в потоках глобальних змінних і змінних інших об'єктів.

Якщо ви звертаєтеся до полів або методів об'єктів VCL, робіть це тільки за допомогою методу Synchronize.

Не «пересинхронізіруйте» ваш додаток, а не те воно працюватиме як один єдиний потік. Надмірно синхронізований додаток втрачає всі переваги від наявності декількох потоків, оскільки вони постійно зупинятимуться і чекатимуть синхронізації. Потоки надають витончене рішення деяких сьогоднішніх проблем програмування; але вони також ускладнюють і без того непростий процес відладки. Та все ж переваги потоків однозначно переважують їх недоліки.

2