І ще для класу 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;
а в потоці для показу повідомлення писати не