Робота з «потоками»в середовищі 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, потрібно вибрати опцію ThreadCount. Так, у момент написання цих рядків MS Word використовував 5 потоків,середовище Delphi – 3.
Якщо задачі додатку можна розділити на різні підмножини: обробкаподій, введення / висновок, зв'язок і ін., то потоки можуть бути органічновбудовані в програмне рішення. Якщо розробник може розділити велику задачу надекілька дрібних, це тільки підвищить переносимість коду і можливості йогобагатократного використовування.
Зробивши додаток багатопотоковим, програміст дістаєдодаткові можливості управління їм. Наприклад, через управління пріоритетамипотоків. Якщо один з них «пригальмовує» додаток, займаючи дуже багатопроцесорний час, його пріоритет може бути знижений.
Інша важлива перевага упровадження потоків – при зростанні «навантаження»на додаток можна збільшити кількість потоків і тим самим зняти проблему.
Потоки спрощують життя тим програмістам, які розробляють додатки вархітектурі клієнт/сервер. Коли потрібне обслуговування нового клієнта, серверможе запустити спеціально для цього окремий потік. Такі потоки прийнятоназивати симетричними потоками (symmetric threads) – вони мають однаковепризначення, виконують один і той же код і можуть розділяти одні і ті жресурси. Більш того, додатки, розраховані на серйозне навантаження, можутьпідтримувати пул (pool) однотипних потоків. Оскільки створення потоку вимагаєпевного часу, для прискорення роботи бажано наперед мати потрібне число готовихпотоків і активізувати їх у міру підключення чергового клієнта.
Примітка
Такий підхід особливо характерний для Web‑серверу MicrosoftInternet 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 andforget).
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. Наприклад, якщопрограміст хоче перед продовженням виконання додатку дочекатися завершеннявідразу декількох потоків, він повинен викликати функцію APIwaitForMuitipieObjects; для її виклику необхідний масив дескрипторів потоків.
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), сповіщення про зміну у файловій системі (changenotification) і консольне введення (console input).
Побічно до цієї групи може бути додана критична секція (criticalsection).
Примітка
Перераховані вище засоби синхронізації в основному інкапсульованідо складу класів 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 і їх поля і властивості.Для підтримки розділення даних між потоками на нижньому рівні в мову ObjectPascal введена спеціальна директива – 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.
· Не «пересинхронізіруйте» ваш додаток, а не те воно працюватиме як одинєдиний потік. Надмірно синхронізований додаток втрачає всі переваги віднаявності декількох потоків, оскільки вони постійно зупинятимуться і чекатимутьсинхронізації. Потоки надають витончене рішення деяких сьогоднішніх проблемпрограмування; але вони також ускладнюють і без того непростий процес відладки.Та все ж переваги потоків однозначно переважують їх недоліки.