Синхронізація процесів при роботі з Windows (документація)

waitable timer (таймер очікування)
Таймер очікування відсутня в windows 95, і для його використання необхідні windows 98 або windows nt 4.0 і вище.


Таймер очікування переходить в сигнальний стан після закінчення заданого інтервалу часу.
Для його створення використовується функція createwaitabletimer:


function createwaitabletimer(
lptimerattributes: psecurityattributes; / / Адреса структури
// tsecurityattributes
bmanualreset: bool; / / Визначає, чи буде таймер переходити в
/ / Сигнальний стан після закінчення функції
/ / Очікування
lptimername: pchar / / Назва об'єкту
): thandle; stdcall;
Коли параметр bmanualreset дорівнює true, то таймер після спрацьовування функції очікування залишається в сигнальному стані до явного виклику setwaitabletimer, якщо false-таймер автоматично переходить в несігнальное стан.


Якщо lptimername збігається з назвою вже існуючого в системі таймера, то функція повертає його ідентифікатор, дозволяючи використовувати об'єкт для синхронізації між процесами. Ім'я таймера не повинно збігатися з ім'ям вже існуючих об'єктів типів event, semaphore, mutex, job або file-mapping.


Ідентифікатор вже існуючого таймера можна отримати функцією:


function openwaitabletimer(
dwdesiredaccess: dword; / / Визначає права доступу до об'єкта
binherithandle: bool; / / Визначає, чи може об'єкт успадковуватися
/ / Дочірніми процесами
lptimername: pchar / / Назва об'єкту
): thandle; stdcall;
Параметр dwdesiredaccess може приймати такі значення:


timer_all_access
Дозволяє повний доступ до об'єкта


timer_modify_state
Дозволяє змінювати стан таймера функціями setwaitabletimer і cancelwaitabletimer


synchronize
Тільки для windows nt – дозволяє використовувати таймер у функціях очікування


Після отримання ідентифікатора таймера потік може задати час його спрацьовування функцією setwaitabletimer:


function setwaitabletimer(
htimer: thandle; / / Ідентифікатор таймера
const lpduetime: tlargeinteger; / / Час спрацювання
lperiod: longint; / / Період повторення спрацьовування
pfncompletionroutine: tfntimerapcroutine; / / Процедура-обробник
lpargtocompletionroutine: pointer; / / Параметр процедури-обробника
fresume: bool / / Визначає, чи буде операційна
/ / Система «прокидатися»
): bool; stdcall;


Розглянемо параметри докладніше.


lpduetime
Задає час спрацьовування таймера. Час задається у форматі tfiletime і базується на coordinated universal time (utc), тобто має бути вказано за Гринвічем. Для перетворення системного часу в tfiletime використовується функція systemtimetofiletime. Якщо час має позитивний знак, воно трактується як абсолютне, якщо негативний – як відносне від моменту запуску таймера.


lperiod
Визначає термін між повторними спрацьовуваннями таймера. Якщо lperiod дорівнює 0, то таймер спрацює один раз.


pfncompletionroutine
Адреса функції, оголошеної як:


procedure timerapcproc(
lpargtocompletionroutine: pointer; / / дані
dwtimerlowvalue: dword; / / молодші 32 розряду значення таймера
dwtimerhighvalue: dword; / / старші 32 розряду значення таймера
); stdcall;
Ця функція викликається, коли спрацьовує таймер, якщо потік, що чекає його спрацьовування, використовує функцію очікування, що підтримує асинхронний виклик процедур. У функцію передаються три параметри:


lpargtocompletionroutine – значення, передане в якості однойменного параметра в функцію setwaitabletimer. Програма може використовувати його для передачі в процедуру обробки адреси блоку даних, необхідних для її роботи
dwtimerlowvalue і dwtimerhighvalue – відповідно члени dwlowdatetime і dwhighdatetime структури tfiletime. Вони описують час спрацьовування таймера. Час задається в utc-форматі (за Гринвічем).
Якщо додаткова функція обробки не потрібна, як цього параметра можна передати nil.


lpargtocompletionroutine
Це значення передається у функцію pfncompletionroutine при її виклику.


fresume
Визначає необхідність «пробудження» системи, якщо на момент спрацьовування таймера вона знаходиться в режимі економії електроенергії (suspended). Якщо операційна система не підтримує пробудження і fresume одно true, то функція setwaitabletimer виконається успішно, однак наступний виклик getlasterror поверне результат error_not_supported.


Якщо необхідно перевести таймер у неактивний стан, це можна зробити функцією:


function cancelwaitabletimer(htimer: thandle): bool; stdcall;
Ця функція не змінює стану таймера і не призводить до спрацьовування функцій очікування і викликом процедур-обробників.


По завершенні роботи об'єкт повинен бути знищений функцією closehandle.


Створимо клас, який очікує в окремому потоці настання заданого часу, а потім викликає процедуру головного потоку додатки. Такий клас може використовуватися, наприклад, в планувальнику завдань (Оскільки таймер очікування дозволяє задавати час спрацьовування в абсолютних величинах, відпадає необхідність постійно аналізувати поточний час, використовуючи звичайний таймер windows):


unit waitthread;


interface


uses classes, windows;


type
twaitthread = class(tthread)
waituntil: tdatetime;
procedure execute; override;
end;


implementation


uses sysutils;


procedure twaitthread.execute;
var
timer: thandle;
systemtime: tsystemtime;
filetime, localfiletime: tfiletime;
begin
timer := createwaitabletimer(nil, false, nil);
try
datetimetosystemtime(waituntil, systemtime);
systemtimetofiletime(systemtime, localfiletime);
localfiletimetofiletime(localfiletime, filetime);
setwaitabletimer(timer, tlargeinteger(filetime), 0,
nil, nil, false);
waitforsingleobject(timer, infinite);
finally
closehandle(timer);
end;
end;


end.


Використовувати цей клас можна, наприклад, наступним чином:


type
tform1 = class(tform)
button1: tbutton;
procedure button1click(sender: tobject);
private
procedure timerfired(sender: tobject);
end;



implementation


uses waitthread;


procedure tform1.button1click(sender: tobject);
var
t: tdatetime;
begin
with twaitthread.create(true) do
begin
onterminate := timerfired;
freeonterminate := true;
/ / Термін очікування закінчиться через 5 секунд
waituntil := now + 1 / 24 / 60 / 60 * 5;
resume;
end;
end;


procedure tform1.timerfired(sender: tobject);
begin
showmessage(“timer fired !”);
end;


Додаткові об'єкти синхронізації
Деякі об'єкти win32 api не призначені виключно для цілей синхронізації, однак можуть використовуватися з функціями синхронізації.
Такими об'єктами є:


Повідомлення про зміну папки (change notification)
windows дозволяє організувати спостереження за змінами об'єктів файлової системи. Для цього служить функція findfirstchangenotification:


function findfirstchangenotification(
lppathname: pchar; / / Шлях до папки, зміни в якій нас
/ / Цікавлять
bwatchsubtree: bool; / / Визначає необхідність стеження за
/ / Змінами у вкладених папках
dwnotifyfilter: dword / / Фільтр подій
): thandle; stdcall;
Параметр dwnotifyfilter – це бітова маска з одного або декількох наступних значень:


file_notify_change_file_name
Слідкування ведеться за будь-якою зміною імені файлу, в тому числі за створенням і видаленням файлів
file_notify_change_dir_name
Слідкування ведеться за будь-якою зміною імені папки, в тому числі за створенням і видаленням папок
file_notify_change_attributes
Слідкування ведеться за будь-якою зміною атрибутів
file_notify_change_size
Слідкування ведеться за зміною розміру файлів. Зміна розміру відбувається при записі у файл. Функція очікування спрацьовує тільки після успішного скидання дискового кешу
file_notify_change_last_write
Слідкування ведеться за зміною часу останнього запису у файл, тобто фактично за будь-який записом у файл. Функція очікування спрацьовує тільки після успішного скидання дискового кешу
file_notify_change_security
Слідкування ведеться за будь-якими змінами дескрипторів захисту


Ідентифікатор, повернутий цією функцією, може використовуватися в будь-якій функції очікування.
Він переходить в сигнальний стан, коли в папці відбуваються зміни, запитані для стеження.
Можна продовжити спостереження, використовуючи функцію findnextchangenotification:


function findnextchangenotification(
hchangehandle: thandle
): bool; stdcall;
По завершенні роботи ідентифікатор повинен бути закритий за допомогою функції findclosechangenotification:


function findclosechangenotification(
hchangehandle: thandle
): bool; stdcall;
Щоб не блокувати виконання основного потоку програми функцією очікування, зручно реалізувати очікування змін в окремому потоці. Реалізуємо потік на базі класу tthread. Для того щоб можна було перервати виконання потоку методом terminate, необхідно, щоб функція очікування, реалізована в методі execute, також переривалася при виклику terminate. Для цього будемо використовувати замість waitforsingleobject функцію waitformultipleobjects і переривати очікування по події (event), що встановлюється в terminate:


type
tcheckfolder = class(tthread)
private
fonchange: tnotifyevent;
handles: array [0 .. 1] of thandle; / / Ідентифікатори об'єктів
/ / Синхронізації
procedure doonchange;
protected
procedure execute; override;
public
constructor create(createsuspended: boolean;
pathtomonitor: string; waitsubtree: boolean;
onchange: tnotifyevent; notifyfilter: dword);
destructor destroy; override;
procedure terminate;
end;


procedure tcheckfolder.doonchange;
/ / Ця процедура викликається в контексті головного потоку програми
/ / У ній можна використовувати виклики vcl, змінювати стан форми,
/ / Наприклад перечитати вміст tlistbox, що відображає файли
begin
if assigned(fonchange) then
fonchange(self);
end;


procedure tcheckfolder.terminate;
begin
inherited; / / Викликаємо tthread.terminate, встановлюємо
// terminated = true
setevent (handles [1]); / / сигналізуючи про необхідність
/ / Перервати очікування
end;


constructor tcheckfolder.create(createsuspended: boolean;
pathtomonitor: string; waitsubtree: boolean;
onchange: tnotifyevent; notifyfilter: dword);
var
boolforwin95: integer;
begin
/ / Створюємо потік зупиненим
inherited create(true);
/ / Windows 95 містить не дуже коректну реалізацію функції
/ / Findfirstchangenotification. Для коректної роботи необхідно,
/ / Щоб:
/ / – Lppathname – не містив завершального слеша "" для
/ / Некореневого каталогу
/ / – Bwatchsubtree – true повинен передаватися як bool (1)
if waitsubtree then
boolforwin95 := 1
else
boolforwin95 := 0;
if (length(pathtomonitor) > 1) and
(pathtomonitor[length(pathtomonitor)] = ‘’) and
(pathtomonitor[length(pathtomonitor)-1] <> ‘:’) then
delete(pathtomonitor, length(pathtomonitor), 1);
handles[0] := findfirstchangenotification(
pchar(pathtomonitor), bool(boolforwin95), notifyfilter);
handles[1] := createevent(nil, true, false, nil);
fonchange := onchange;
/ / І, при необхідності, запускаємо
if not createsuspended then
resume;
end;


destructor tcheckfolder.destroy;
begin
findclosechangenotification(handles[0]);
closehandle(handles[1]);
inherited;
end;


procedure tcheckfolder.execute;
var
reason: integer;
dummy: integer;
begin
repeat
/ / Очікуємо зміни в папці якого сигналу про завершення
/ / Потоку
reason := waitformultipleobjects(2, @handles, false, infinite);
if reason = wait_object_0 then begin
/ / Змінилася папка, викликаємо обробник у контексті
/ / Головного потоку програми
synchronize(doonchange);
/ / І продовжуємо пошук
findnextchangenotification(handles[0]);
end;
until terminated;
end;


Оскільки метод tthread.terminate не віртуальний, цей клас не можна використовувати зі змінною типу tthread, так як в цьому випадку буде викликатися метод terminate класу tthread, який не може перервати очікування, і потік буде виконуватися до зміни в папці, за якою ведеться спостереження.


Пристрій стандартного введення з консолі (console input)
Ідентифікатор стандартного пристрою введення з консолі, отриманий за допомогою виклику функції getstdhandle (std_input_handle), можна використовувати у функціях очікування. Він знаходиться в сигнальному стані, якщо черга введення консолі не порожня, і в несігнальном – якщо порожня. Це дозволяє організувати очікування введення символів або за допомогою функції waitformultipleobjects поєднати його з очікуванням будь-яких інших подій.


Завдання (job)
job – це новий механізм windows 2000, що дозволяє об'єднати групу процесів в одне завдання і маніпулювати ними одночасно. Ідентифікатор завдання знаходиться в сигнальному стані, якщо всі процеси, асоційовані з ним, завершилися з причини закінчення ліміту часу на виконання завдання.


Процес (process)
Ідентифікатор процесу, отриманий за допомогою функції createprocess, переходить в сигнальний стан після завершення процесу, що дозволяє організувати очікування завершення процесу, наприклад, при запуску з програми зовнішньої програми:


var
pi: tprocessinformation;
si: tstartupinfo;

fillchar(si, sizeof(si), 0);
si.cb := sizeof(si);
win32check(createprocess(nil, “command.com”, nil,
nil, false, 0, nil, nil, si, pi));
/ / Затримуємо виконання програми до завершення процесу
waitforsingleobject(pi.hprocess, infinite);
closehandle(pi.hprocess);
closehandle(pi.hthread);
Слід розуміти, що в цьому випадку викликає процес буде заморожений повністю і не зможе обробляти повідомлення. Тому, якщо дочірній процес може виконуватися протягом тривалого часу, краще використовувати більш коректний варіант очікування, описаний в розділі, присвяченому функції msgwaitformultipleobjects.


Потік (thread)
Ідентифікатор потоку знаходиться в несігнальном стані до тих пір, поки потік виконується. По його завершенні ідентифікатор переходить в сигнальний стан. Це дозволяє легко дізнатися, завершився чи потік, або за допомогою функції, яка чекає на декілька об'єктів, організувати очікування завершення одного або всіх цікавлять потоків.


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


procedure initializecriticalsection(
var lpcriticalsection: trtlcriticalsection
); stdcall;
Після створення об'єкту потік, перед доступом до захищається ресурсу, повинен викликати функцію:


procedure entercriticalsection(
var lpcriticalsection: trtlcriticalsection
); stdcall;
Якщо в цей момент жоден з потоків в процесі не володіє об'єктом, то потік стає власником критичної секції і продовжує виконання. Якщо секція вже захоплена іншим потоком, то виконання потоку, викликав функцію, зупиняється до її звільнення.


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


procedure leavecriticalsection(
var lpcriticalsection: trtlcriticalsection
); stdcall;
Ця функція звільняє об'єкт незалежно від кількості попередніх викликів потоком функції entercriticalsection. Якщо є інші потоки, які очікують звільнення секції, один з них стає її власником і продовжує виконання. Якщо потік завершився, не звільнивши критичну секцію, то її стан стає невизначеним, що може викликати блокування роботи програми.


Є можливість спробувати захопити об'єкт без заморожування потоку. Для цього служить функція tryentercriticalsection:


function tryentercriticalsection(
var lpcriticalsection: trtlcriticalsection
): bool; stdcall;
Вона перевіряє, захоплена чи секція в момент її виклику. Якщо так – функція повертає false, в іншому випадку – захоплює секцію і повертає true.


По завершенні роботи з критичною секцією вона повинна бути знищена викликом функції deletecriticalsection:


procedure deletecriticalsection(
var lpcriticalsection: trtlcriticalsection
); stdcall;
Розглянемо приклад програми, що здійснює в декількох потоках завантаження даних по мережі. Глобальні змінні bytessummary і timesummary зберігають загальна кількість завантажених байтів і час завантаження. Ці змінні кожен потік оновлює у міру зчитування даних; для запобігання конфліктів додаток повинен захистити загальний ресурс за допомогою критичної секції:


var
/ / Глобальні змінні
criticalsection: trtlcriticalsection;
bytessummary: cardinal;
timesummary: tdatetime;
averagespeed: float;

/ / При ініціалізації програми
initializecriticalsection(criticalsection);
bytessummary := 0;
timesummary := 0;
averagespeed := 0;


/ / У методі execute потоку, що завантажує дані.
repeat
bytesread := readdatablockfromnetwork;
entercriticalsection(criticalsection);
try
bytessummary := bytessummary + bytesread;
timesummary := timesummary + (now – threadstarttime);
if timesummary > 0 then
averagespeed := bytessummary / (timesummary/24/60/60);
finally
leavecriticalsection(criticalsection)
end;
until loadcomplete;


/ / При завершенні програми
deletecriticalsection(criticalsection);
delphi надає клас, інкапсулює функціональність критичної секції. Клас оголошений в модулі syncobjs.pas:


type
tcriticalsection = class(tsynchroobject)
public
constructor create;
destructor destroy; override;
procedure acquire; override;
procedure release; override;
procedure enter;
procedure leave;
end;
Методи enter і leave є синонімами методів acquire і release відповідно і додані для кращої читаності вихідного коду:


procedure tcriticalsection.enter;
begin
acquire;
end;


procedure tcriticalsection.leave;
begin
release;
end;


Захищений доступ до змінних (interlocked variable access)
Часто виникає необхідність у здійсненні операцій над розділяються між потоками 32-розрядними змінними. З метою спрощення рішення цього завдання windows api надає функції для захищеного доступу до них, які не потребують використання додаткових (і більш складних) механізмів синхронізації. Змінні, що використовуються в цих функціях, повинні бути вирівняні на кордон 32-розрядного слова. Стосовно до delphi це означає, що якщо змінна оголошена усередині запису (record), то цей запис не повинна бути упакованої (packed) і при її оголошенні повинна бути активна директива компілятора {$ a +}. Недотримання даної вимоги може призвести до виникнення помилок на багатопроцесорних конфігураціях.


type
tpackedrecord = packed record
a: byte;
b: integer;
end;
/ / Tpackedrecord.b не можна використовувати у функціях interlockedxxx


tnotpackedrecord = record
a: byte;
b: integer;
end;


{$a-}
var
a1: tnotpackedrecord;
/ / A1.b не можна використовувати у функціях interlockedxxx
i: integer
/ / I можна використовувати у функціях interlockedxxx, так як змінні в
/ / Delphi завжди вирівнюються на кордон слова безвідносно
/ / До стану директиви компілятора $ a


{$a+}
var
a2: tnotpackedrecord;
/ / A2.b можна використовувати у функціях interlockedxxx


function interlockedincrement(
var addend: integer
): integer; stdcall;
Функція збільшує змінну addend на 1. Значення, що повертається залежить від операційної системи:


windows 98, windows nt 4.0 і старше – повертається нове значення змінної addend;
windows 95, windows nt 3.51:


якщо після зміни addend <0, то повертається негативне число, не обов'язково однакову addend;
якщо addend = 0, то повертається 0;
якщо після зміни addend> 0, то повертається позитивне число, не обов'язково однакову addend.


function interlockeddecrement(
var addend: integer
): integer; stdcall;
Функція зменшує змінну addend на 1. Значення, що повертається аналогічно функції interlockedincrement.


function interlockedexchange(
var target: integer;
value: integer
): integer; stdcall;
Функція записує в змінну target значення value і повертає попереднє значення target.


Наступні функції для виконання потребують windows 98 або windows nt 4.0 і старше.


function interlockedcompareexchange(
var destination: pointer;
exchange: pointer;
comperand: pointer
): pointer; stdcall;
Функція порівнює значення destination і comperand. Якщо вони збігаються, значення exchange записується в destination. Функція повертає початкове значення destination.


function interlockedexchangeadd(
addend: plongint;
value: longint
): longint; stdcall;
Функція додає до змінної, на яку вказує addend, значення value і повертає початкове значення addend.


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


Якщо додатки або потоки одного процесу змінюють загальний ресурс – захищайте доступ до нього за допомогою критичних секцій або м'ютексів.
Якщо доступ здійснюється тільки на читання – захищати ресурс не обов'язково
Критичні секції більш ефективні, але застосовані тільки усередині одного процесу; м'ютекси можуть використовуватися для синхронізації між процесами.
Використовуйте семафори для обмеження кількості звернень до одного ресурсу.
Використовуйте події (event) для інформування потоку про настання якої-небудь події.
Якщо розділяється ресурс – 32-бітова мінлива, то для синхронізації доступу до нього можна використовувати функції, що забезпечують розподілений доступ до змінних.
Багато об'єктів win32 дозволяють організувати ефективне спостереження за своїм станом за допомогою функцій очікування. Це найбільш ефективний з точки зору витрат системних ресурсів метод.
Якщо ваш потік створює (навіть неявно, за допомогою coinitialize або функцій dde) вікна, то він повинен обробляти повідомлення. Не використовуйте в такому потоці функції, що не дозволяють перервати очікування по приходу повідомлення з більшим чи необмеженим періодом очікування. Використовуйте функції msgwaitforxxx.

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


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

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

Ваш отзыв

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

*

*