Контрольна робота
з інформатики
Особливості багатозадачності в середовищі Windows
Вступ
Основні поняття багатозадачності в Windows 95 - процес (задача) і потік (нитка). Під процесом розуміється виконання програми в цілому (WinWord, Excel, Visual C++ і т.д.) Потоками у свою чергу є частини процесу, що виконуються паралельно.
Процесом звичайно називають екземпляр програми, що виконується.
Хоча на перший погляд здається, що програма і процес поняття практично однакові, вони фундаментально відрізняються один від одного. Програма представляє собою статичний набір команд, а процес це набір ресурсів і даних, що використовуються при виконанні програми. Процес в Windows складається з наступних компонентів:
- Структура даних, що включає в себе всю інформацію про процес, в тому числі список відкритих дескрипторів різних системних ресурсів, унікальний ідентифікатор процесу, різноманітну статистичну інформацію і т.д.;
- Адресний простір - діапазон адресів віртуальної пам‘яті, яким може користуватися процес;
- Програма, що виконується і дані, що проектуються на віртуальний адресний простір процесу.
Будь який процес має хоча б один потік (у цьому випадку його можна ототожнити з потоком). Це первинний потік створюється системою автоматично при створенні процесу. Далі цей потік може породити інші потоки, ті в свою чергу нові і т.д. Таким чином, один процес може володіти декількома потоками, і тоді вони одночасно виконують код в адресному просторі процесу.
Windows краще всього працює, коли всі потоки можуть займатися своїм ділом, не взаємодіючи один з одним. Але така ситуація дуже рідкісна. Звичайно потік створюється для виконання певної роботи, про завершення якої, ймовірно, захоче узнати інший потік.
Приклад: один потік підготовляє дані, інший їх сортує, а третій виводить результат у файл. Передавши готові дані другому потоку на сортування, перший починає обробку нового блоку. Тим часом другий потік повідомляє третьому, що можна виводити результати. Роботу цих трьох потоків необхідно синхронізувати.
Всі потоки в системі повинні мати доступ до системних ресурсів — кучам, послідовним портам, файлам, вікнам і т д. Якщо один із потоків запросить монопольний доступ до якого-небудь ресурсу, іншим потокам, яким теж потрібен цей ресурс, не вдасться виконати свої задачі. А с другої сторони, просто недопустимо, щоб потоки безконтрольно користувались ресурсами. Інакше може статися так, що один потік пише в блок пам‘яті, з якого інший щось зчитує.
Потоки повинні взаємодіяти один з одним в двох основних випадках:
1) спільно використовуючи один і той же ресурс (щоб не розрушити його);
2) коли треба повідомити інші потоки про завершення яких-небудь операцій
В Windows є маса засобів, що спрощують синхронізацію потоків. Але точно спрогнозувати, в який момент потоки будуть робити то-то и то-то, надзвичайно складно.
Механізми синхронізації
Найбільш простим механізмом синхронізації є використання Interlocked-функцій. Використання цих функцій гарантує “атомарне” виконання потрібних операцій, тобто потоки не будуть заважати один одному.
Пояснимо на прикладі:
// definition of global viriable lorig g_x = 0;
DWORD WINAPI ThreadFunc1(PVOID pvParam) {
g_x++;
return(0); }
DWORD WINAPI ThreadFunc2(PVOID pvParam} {
g_x++;
return(0); }
Нема ні якої впевненості, що отримаємо двійку, тому що ми не управляємо механізмом витіснення потоків.
// definition of global viriable long g_x = 0;
DWORD WINAPI ThreadFunc1(PVOID pvParam) {
InterlockedExchangeAdd(&g_x, 1);
return(0); }
DWORD WINAPI ThreadFunc2(PVOID pvPararr) {
InterlockedExchangeAdd(&g_x, 1);
return(0); }
Тут вже можна бути впевненим, що значення g_x=2.
Розглянемо ці функції більш детально
Якщо кілька потоків мають доступ до однієї змінного, те немає ніякої гарантії, що в процесі зміни значення цієї змінний одним потоком не відбудеться переключення на інший потік, що може читати її значення. Інший потік у цьому випадку одержить невірну інформацію. Для запобігання таких конфліктів у Windows 95 уведений ряд функцій, що дозволяють коректно змінювати змінні, доступ до яких мають кілька потоків. Перелічимо функції, що охороняють від переключення під час зміни значення змінної:
LONG InterlockedIncrement (LPLONG lpAddend) - збільшує значення за адресою lpAddend на одиницю;
LONG InterlockedDecrement (LPLONG lpAddend) - зменшує значення за адресою lpAddend на одиницю;
LONG InterlockedExchange (LPLONG Target, LONG Value) - заміняє значення, що знаходиться за адресою Target, на значення, передане в параметрі Value;
LONG InterlockedExchangeAdd (PLONG Addend, LONG Increment) - додає до значення за адресою Addend значення Increment;
PVOID InterlockedCompareExchange (PVOID *Destination, PVOID Exchange, PVOID Comperand) - порівнює значення за адресою Destination зі значенням, переданим у параметрі Comperand, і якщо ці значення рівні, то за адресою Destination міститься значення, передане в параметрі Exchange.
Іншими словами, якщо в тексті програми є загальна змінна, те її зміна повинна вироблятися в такий спосіб:
{ long Val;
....
Val++; // неправильно
InterlockedIncrement(&Val); // правильно
...
}
Усі спроби зробити щось, що вимагає моментальної реакції на зовнішні події, у середовищі Windows 3.x приводили до більш ніж скромних результатів, тому що подібні програми здобували відносно стандартизований, але неповороткий графічний інтерфейс, і більше нічого. Windows 95 у принципі дозволяє розробляти критичне вчасно реакції ПО типу систем керування.
Цей метод хороший для дуже простих речей, для більш складної синхронізації він не допоможе. На щастя у Windows передбачено п'ять стандартних механізмів для синхронізації процесів і потоків:
семафор
критична секція
м’ютекс
подія
таймер
Розглянемо кожний з цих механізмів.
Критична секція
Критична секція - це частина коду, доступ до якого тепер має тільки один потік. Інший потік може звернутися до критичного розділу, тільки коли перший вийде з нього.
Для роботи з критичними секціями використовуються наступні функції:
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection) - ініціалізація синхронізатора типу критичний розділ.
lpCriticalSection - покажчик на змінну типу CRITICAL_SECTION.
VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection) - запит на вхід у критичну секцію(розділ)
lpCriticalSection - покажчик на змінну типу CRITICAL_SECTION.
VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection) - вихід із критичного розділу (звільнення семафора).
lpCriticalSection - покажчик на змінну типу CRITICAL_SECTION.
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection) - видалення критичного розділу (звичайно при виході з програми).
lpCriticalSection - покажчик на змінну типу CRITICAL_SECTION.
Отже, для створення критичного розділу необхідно ініціалізувати структуру CRITICAL_SECTION. Що Windows у цій структурі зберігає, нас не стосується - важливо, що покажчик на цю структуру ідентифікує наш семафор.
Створивши об'єкт CRITICAL_SECTION, ми можемо працювати з ним, тобто можемо позначити код, доступ до якого для одночасно виконуються задач потрібно синхронізувати.
Розглянемо такий приклад. Ми хочемо записувати і зчитувати значення з деякого глобального масиву mas. Причому запис і зчитування повинні вироблятися двома різними потоками. Цілком природно, що краще якщо ці дії не будуть виконуватися одночасно. Тому введемо обмеження на доступ до масиву.
І хоча приведений нами приклад подібного обмеження (см
. лісти
н
г 1)надзвичайно спрощений, він добре ілюструє роботу синхронізатора типу критичний розділ: поки один потік "володіє" масивом, інший доступу до нього не має.
М‘ютекси (взаємовиключення)
М’ютекс (взаємовиключення, mutex) - це об’єкт синхронізації, який установлюється в особливий сигнальний стан, коли не зайнятий яким-небудь потоком. Тільки один потік володіє цим об’єктом в любий момент часу, звідси и назва таких об‘єктів – одночасний доступ до спільного ресурсу виключається. Наприклад, щоб виключити запис двох потоків в спільний участок пам’яті в один і то й же час, кожний потік очікує, коли звільниться м’ютекс, стає його власником и тільки потім пише щось в цю ділянку пам’яті. Після всіх необхідних дій м’ютекс звільняється, надаючи іншим потокам доступ до спільного ресурсу.
Два (або більше) процесів можуть створити м‘ютекс з одним і тим же іменем, визвавши метод CreateMutex. Перший процес дійсно створює м’ютекс, а наступні процеси отримують хендл існуючого вже об‘єкта. Це дає можливість декільком процесам отримати хендл одного і того ж м’ютекса, звільняючи програміста від необхідності турбуватися про те, хто насправді створює м’ютекс. Якщо використовується такий підхід, бажано встановити флаг bInitialOwner в FALSE, інакше виникнуть певні труднощі при визначенні справжнього “творця” м’ютекса.
Декілька процесів можуть отримати хендл (handle) одного й того ж м‘ютекса, що робить можливим взаємодію між процесами. Ви можете використовувати наступні механізми такого підходу:
Дочірній процес, створений за допомогою функції CreateProcess може наслідувати хендл м‘ютекса у випадку, якщо при його (м‘ютекса) створенні функцією CreateMutex був вказаний параметр lpMutexAttributes.
Процес може отримати дублікат існуючого м‘ютекса з допомогою функції DuplicateHandle.
Процес може вказати ім‘я існуючого м‘ютекса при виклику функцій OpenMutex або CreateMutex.
#include <windows.h>
#include <process.h>
#include <stdio.h>
HANDLE hMutex;
int a[ 5 ];
void Thread(void* pParams)
{
int i, num = 0;
while (TRUE)
{
WaitForSingleObject(hMutex, INFINITE);
for (i = 0; i < 5; i++) a[ i ] = num;
ReleaseMutex(hMutex);
num++;
}
}
int main(void)
{
hMutex = CreateMutex(NULL, FALSE, NULL);
_beginthread(Thread, 0, NULL);
while(TRUE)
{
WaitForSingleObject(hMutex, INFINITE);
printf("%d %d %d %d %dn",
a[ 0 ], a[ 1 ], a[ 2 ],
a[ 3 ], a[ 4 ]);
ReleaseMutex(hMutex);
}
return 0;
}
Як видно з результату роботи процесу, основний потік (сама програма) і потік hMutex дійсно працюють паралельно (червоним кольором позначений стан, коли основний потік виводить масив під час його заповнення потоком hMutex):
81751652 81751652 81751651 81751651 81751651
81751652 81751652 81751651 81751651 81751651
83348630 83348630 83348630 83348629 83348629
83348630 83348630 83348630 83348629 83348629
83348630 83348630 83348630 83348629 83348629
Приклад. Допустимо, в програмі використовується ресурс, наприклад, файл або буфер в пам‘яті. Функція WriteToBuffer() викликається з різних потоків. Щоб уникнути колізій при одночасному зверненні до буферу з різних потоків, використовуємо м‘ютекс. Перед тим як звернутися до буфера, очікуємо „звільнення” м‘ютекса.
HANDLE hMutex;
int main()
{
hMutex = CreateMutex(NULL, FALSE, NULL); // Создаем мьютекс в свободном состоянии
//...
// Создание потоков, и т.д.
//...
}
BOOL WriteToBuffer()
{ DWORD dwWaitResult;
// Ждем освобождения мьютекса перед тем как обратиться к буферу.
dwWaitResult = WaitForSingleObject(hMutex, 5000L); // 5 секунд на таймаут
if (dwWaitResult == WAIT_TIMEOUT) // Таймаут. Мьютекс за єто время не освободился.
{
return FALSE;
}
else // Мьютекс освободился, и наш поток его занял. Можно работать.
{
Write_to_the_buffer().
...
ReleaseMutex(hMutex); // Освобождаем мьютекс.
}
return TRUE;
Семафор
Ще один вид синхронізаторів - семафор, що виключає. Основна його відмінність від критичної секції полягає в тім, що останню можна використовувати тільки в межах одного процесу (одного запущеного додатка), а семафорами, що виключають, можуть користатися різні процеси.
Semaphore – глобальний об’єкт синхронізації, що має лічильник для ресурсів, з ним пов‘язаних. В достатньо грубому приближенні м‘ютекс можна розглядати, як частковий випадок семафора з двома станами.
Іншими словами, критичні розділи - це локальні семафори, що доступні в рамках тільки однієї програми, а семафори, що виключають, можуть бути глобальними об'єктами, що дозволяють синхронізувати роботу програм (тобто різні запущені додатки можуть розділяти ті самі дані).
Розглянемо основні функції семафора, що виключає, на прикладі роботи з об'єктами mutex.
1. Створення об'єкта mutex
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName)
lpMutexAttributes - покажчик на структуру SECURITY_ATTRIBUTES (у Windows 95 даний параметр ігнорується);
bInitialOwner - указує первісний стан створеного об'єкта (TRUE - об'єкт відразу стає зайнятим, FALSE - об'єкт вільний);
lpName - указує на рядок, що містить ім'я об'єкта. Ім'я необхідне для доступу до об'єкта інших процесів, у цьому випадку об'єкт стає глобальним і їм можуть оперувати різні програми. Якщо вам не потрібний іменований об'єкт, то вкажіть NULL. Функція повертає покажчик на об'єкт mutex. Надалі цей покажчик використовується для керування семафором, що виключає.
2. Закриття (знищення) об'єкта mutex
BOOL CloseHandle(HANDLE hObject)
3. Універсальна функція запиту доступу
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds) - універсальна функція, призначена для запиту доступу до синхронізуючого об'єкту (у даному випадку до об'єкта mutex).
hHandle - покажчик на синхронізуючий об'єкт (у даному випадку передається значення, повернуте функцією CreateMutex);
dwMilliseconds - час (у міллісекундах), протягом якого відбувається чекання звільнення об'єкта mutex. Якщо передати значення INFINITE (нескінченність), то функція буде чекати нескінченно довго.
Дана функція може повертати наступні значення:
WAIT_OBJECT_0 - об'єкт звільнився;
WAIT_TIMEOUT - час чекання звільнення пройшов, а об'єкт не звільнився;
WAIT_ABANDON - відбулося відмовлення від об'єкта (тобто процес, що володіє даним об'єктом, завершилося, не звільнивши об'єкт). У цьому випадку система (а не "процес-власник") переводить об'єкт у вільний стан. Таке звільнення об'єкта не припускає гарантій у захищеності даних;
WAIT_FAILED - відбулася помилка.
4. Звільнення об'єкта mutex
BOOL ReleaseMutex(HANDLE hMutex) - звільняє об'єкт mutex, переводячи його з зайнятого у вільний стан.
Чи дійде черга до вас?
Отже, якщо програмі необхідно ввійти в поділюваний код, то вона запитує дозвіл шляхом виклику функції WaitForSingleObject. При цьому якщо об'єкт синхронізації зайнятий, то виконання запитуючого потоку припиняється і невикористана частина відведеного часу передається іншому потоку. А тепер увага! Теоретично: як тільки об'єкт стає вільним, що очікує потік відразу захоплює його. Але це тільки теоретично. На практиці цього не відбувається. Захоплення об'єкта, що звільнився, відбувається лише тоді, що коли очікує потік знову одержить свій квант часу. І тільки тоді він зможе перевірити, чи звільнився об'єкт, і, якщо так, захопити його.
Непрямим підтвердженням вищевикладених міркувань може служити той факт, що Microsoft не передбачила підтримку черговості запитів на доступ до об'єкта синхронізації. Тобто якщо кілька процесів очікують звільнення того самого об'єкта синхронізації, то немає ніякої можливості довідатися, який саме з них першим одержить доступ до об'єкта, що звільнився.
Пояснимо це на наступному прикладі. Нехай, трьом потокам необхідно звернутися до однієї ділянки коду, причому одноразово ця ділянка повинна виконувати тільки один потік. Введемо об'єкт синхронізації mutex, що регулює доступ потоків до цієї ділянки коду. Коли потік 1 захопив об'єкт mutex і став виконувати поділювану ділянку коду, потік 2 запросив дозвіл на доступ (тобто викликав функцію WaitForSingleObject), а система перевела потік 2 у режим чекання. Через якийсь час потік 3 теж запросив дозвіл на вхід у цей код і теж перейшов у режим чекання. Тепер, якщо потік 1 звільнить об'єкт синхронізації, те невідомо, який потік (2 чи 3) його захопить, - усі залежить від того, хто з них першим одержить свій квант часу для продовження роботи. Нехай об'єктом синхронізації заволодів потік 3, а поки він виконував поділюваний розділ, потік 1 знову запросив доступ до об'єкта синхронізації - і знову стало два конкуруючих потоки (1 і 2). І хто з них першим "достукається" до ділянки коду, що виконується, невідомо: може случитися так, що потік 2 ніколи не буде допущений до бажаної ділянки коду і надовго залишиться в стані чекання... А як відомо, гірше немає чекати, хоча потоку це байдуже. Інша справа - вам...
Події
Подія - це об‘єкт синхронізації, стан якого може бути установлений сигнальним шляхом виклику функцій SetEvent або PulseEvent. Існує два типа подій:
Тип об‘єкту | Опис |
Подія з ручним “сбросом” | Це об‘єкт, сигнальний стан якого зберігається до ручного” сброса” функцією ResetEvent. Як тільки стан об‘єкту установлений в сигнальний, всі потоки, що знаходяться в циклі очікування цього об‘єкту, продовжують своє виконання (звільняються). |
Подія з автоматичним “сбросом” | Об‘єкт, сигнальний стан якого зберігається до тих пір, поки не буде звільнений єдиний потік, після чого система автоматично установлює несигнальний стан події. Якщо нема потоків, очікуючих цієї події, об‘єкт залишається в сигнальному стані. |
Події корисні в тих випадках, коли необхідно послати повідомлення потоку, яке сповіщає, що відбулася певна подія. Наприклад, при асинхронних операціях вводу и виводу з одного пристрою, система установлює подію в сигнальний стан коли закінчується якась із цих операцій. Один потік може використовувати декілька різних подій в декількох операціях, що перекриваються, а потім очікувати приходу сигналу від любого з них.
Потік може використовувати функцію CreateEvent для створення об‘єкту подія. Створюючий подію потік встановлює її початковий стан. В цьому потоці можна вказати ім‘я події. Потоки інших процесів можуть отримати доступ до цієї події по імені, вказав його у функції OpenEvent.
Потік може використовувати функцію PulseEvent для установки стану події - сигнальним і потім “сбросить” стан в несигнальне значення після звільнення відповідної кількості очікуючих потоків. У випадку об‘єктів з ручним “сбросом” звільняються всі очікуючі потоки. У випадку об‘єктів з автоматичним “сбросом” звільняється тільки єдиний потік, навіть якщо цієї події очікують декілька потоків. Якщо очікуючих потоків нема, PulseEvent просто встановлює стан подій - несигнальний.
#include <windows.h>
#include <process.h>
#include <stdio.h>
HANDLE hEvent1, hEvent2;
int a[ 5 ];
void Thread(void* pParams)
{
int i, num = 0;
while (TRUE)
{
WaitForSingleObject(hEvent2, INFINITE);
for (i = 0; i < 5; i++) a[ i ] = num;
SetEvent(hEvent1);
num++;
}
}
int main(void)
{
hEvent1 = CreateEvent(NULL, FALSE, TRUE, NULL);
hEvent2 = CreateEvent(NULL, FALSE, FALSE, NULL);
_beginthread(Thread, 0, NULL);
while(TRUE)
{
WaitForSingleObject(hEvent1, INFINITE);
printf("%d %d %d %d %dn",
a[ 0 ], a[ 1 ], a[ 2 ],
a[ 3 ], a[ 4 ]);
SetEvent(hEvent2);
}
return 0;
}
Таймери, що чекають
Мабуть, таймери, що очікують - самий витончений об‘єкт ядра для синхронізації. З‘явились вони, починаючи з Windows 98. Таймери створюються функцією CreateWaitableTimer і бувають, також як і події, з автосбросом і без нього. Потім таймер треба настроїти функцією SetWaitableTimer. Таймер переходить в сигнальний стан, коли закінчується його таймаут. Відмінити "цокання" таймера можна функцією CancelWaitableTimer. Відмітимо, що можна указати callback функцію при установці таймера. Вона буде виконуватись, коли спрацьовує таймер.
Приклад. Напишемо програму-будильник використовуючи WaitableTimer'и. Будильник буде спрацьовувати раз в день в 8 ранку и "пікати" 10 раз. Використовуємо для цього два таймера, один з яких с callback-функцією.
#include <process.h>
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#define HOUR (8) // the time of alarm
#define RINGS (10) // number of rings
HANDLE hTerminateEvent;
// callback – timer function
VOID CALLBACK TimerAPCProc(LPVOID, DWORD, DWORD)
{
Beep(1000,500); // ringing!
};
// thread function
unsigned __s
{
HANDLE hDayTimer = CreateWaitableTimer(NULL,FALSE,NULL);
HANDLE hAlarmTimer = CreateWaitableTimer(NULL,FALSE,NULL);
HANDLE h[2]; // we will wait for these objects
h[0] = hTerminateEvent; h[1] = hDayTimer;
int iRingCount=0; // number "rings"
int iFlag;
DWORD dw;
// we should convert the time into FILETIME format
// timer don’t understand other formats
LARGE_INTEGER liDueTime, liAllDay;
liDueTime.QuadPart=0;
// day in 100-nanosecond intervals = 10000000 * 60 * 60 * 24 = 0xC92A69C000
liAllDay.QuadPart = 0xC9;
liAllDay.QuadPart=liAllDay.QuadPart << 32;
liAllDay.QuadPart |= 0x2A69C000;
SYSTEMTIME st;
GetLocalTime(&st); // current day and time
iFlag = st.wHour > HOUR; // if the Time don’t come
// than we set alarm for today, if not than for tomorrow
st.wHour = HOUR;
st.wMinute = 0;
st.wSecond =0;
FILETIME ft;
SystemTimeToFileTime(&st, &ft);
if (iFlag)
((LARGE_INTEGER *)&ft)->QuadPart =
((LARGE_INTEGER *)&ft)->QuadPart +liAllDay.QuadPart;
LocalFileTimeToFileTime(&ft,&ft);
// Installing the timer,
// it will alarm once a day
SetWaitableTimer(hDayTimer, (LARGE_INTEGER *) &ft, 24*60*60000, 0, 0, 0);
do {
dw = WaitForMultipleObjectsEx(2,h,FALSE,INFINITE,TRUE);
if (dw == WAIT_OBJECT_0 +1) // hDayTimer
{
SetWaitableTimer(hAlarmTimer, &liDueTime, 1000, TimerAPCProc, NULL, 0);
iRingCount=0;
}
if (dw == WAIT_IO_COMPLETION) // the callback-functionhas finished working
{
iRingCount++;
if (iRingCount==RINGS)
CancelWaitableTimer(hAlarmTimer);
}
}while (dw!= WAIT_OBJECT_0); // while hTerminateEvent is of
CancelWaitableTimer(hDayTimer);
CancelWaitableTimer(hAlarmTimer);
CloseHandle(hDayTimer);
CloseHandle(hAlarmTimer);
_endthreadex(0);
return 0;
};
int main(int argc, char* argv[])
{
// this event shows the thread when to finish working
hTerminateEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
unsigned uThreadID;
HANDLE hThread;
// creating thread
hThread = (HANDLE)_beginthreadex(NULL, 0, &ThreadFunc, 0, 0,&uThreadID);
puts("Press any key to exit.");
getch();
// setting the event
SetEvent(hTerminateEvent);
//waiting for closing of the thread
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return 0;}
Багатопотоковість і графіка
Є ще одна особливість при роботі з об'єктами синхронізації. Справа в тім, що Windows 95 досить "важко" взаємодіє зі своєю графічною системою в багатозадачному режимі. Це пояснюється тим, що в Windows 95 графічна підсистема частково залишилася 16-розрядної і звертання до такого коду приводить до захоплення системного семафора, що виключає, Win16Mutex, що запобігає одночасний доступ декількох процесів (потоків) до такого коду. Твердження авторів деяких книг по Windows 95 про те, що це не є перешкодою, якщо ви працюєте в цілком 32-розрядному додатку, на практиці виявляється неспроможним.
Отже, основною проблемою стала неможливість коректного зняття з виконання графічного потоку. Зняття вироблялося по наступному алгоритму. Кожен потік у нескінченному циклі перевіряв прапор-сигнал про завершення. Якщо прапор був виставлений, то потік виходив з нескінченного циклу і завершувався штатним шляхом. У спрощеному виді процедура зняття описана в ліс
тингу 3.
Такий код ідеально працював, якщо вироблялося зняття потоку, що не звертається до графічної системи Windows (або рідко звертається - раз у кілька секунд). Якщо ж потік увесь час що-небудь малював, то спроба зняття закінчувалася виходом з функції WaitForSingleObject через перевищення часу чекання (значення, щоповертається, WAIT_TIMEOUT), тобто підпрограма, що знімається, не одержувала керування, поки ми "сиділи" у функції WaitForSingleObject. Збільшення періоду чекання (наприклад, до 10 с) ні до чого не приводило - потік усі десять секунд уперто чекав звільнення об'єкта і зрештою виходив зі значенням WAIT_TIMEOUT.
Причина, по якій потік не знімався, узагалі ж зрозуміла - йому не передавалося керування. Можна спробувати примусово зробити це, збільшивши пріоритет потоку, що знімається:
void breakTask(GF_Task* tsk)
{
DWORD result;
char s[512];
// команда потоку, що знімається, на зняття tsk->putState(tsBreak,True);
// збільшуємо відносний пріоритет
// потоку, що знімається, до максимально можливого
SetThreadPriority(tsk->TaskHnd95, THREAD_PRIORITY_TIME_CRITICAL)
// чекаємо завершення потоку протягом 1 з
WaitForSingleObject(tsk->TaskHnd95,1000);
}
Результату ніякого (вірніше, результат той же - вихід зі значенням WAIT_TIMEOUT). Виходить, що підвищення пріоритету не завжди спрацьовує (ще однією докір Microsoft).
Що ж робити? Як змусити потік, у якому працює програма зняття breakTask, передати керування іншим потокам? При одержанні значення WAIT_TIMEOUT починає виконуватися та частина коду, що виводить на екран вікно з запитом про те, що ж робити з потоком, що не знімається. У момент висновку вікна на екран багатостраждальний потік раптом сам завершується - він нарешті "зауважує" прапорець завершення і виходить з нескінченного циклу. Це підтверджує, що до потоку, що знімається, просто не доходить керування (не виділяється квант часу).
Не вдаючись у причини подібного поводження Windows, ми повинні проаналізувати, а що ж усе-таки відбувається в модальному вікні, що змушує ОС помітити нашу задачу. Імовірно, усе криється в петлі чекання подій, що запускається в модальному вікні. Однієї з основних функцій у такому циклі чекання є функція GetMessage. Чудовою властивістю володіє дана функція: її виклик приводить до оповіщення планувальника задач Windows. Оскільки зовнішніх подій для потоку, що викликав цю функцію, ні, те частину, що залишилася, його кванта часу планувальник задач передає іншому потоку, що виконується. Таким чином, наш потік, що знімається, знову оживає.
Отже, нам треба використовувати функцію типу GetMessage для стимуляції Windows до передачі керування іншим потокам. Але сама функція GetMessage нам не підходить, тому що вона віддає керування тільки в тому випадку, якщо для потоку з'явилося повідомлення. Замість GetMessage можна застосувати функцію PeekMessage, що перевіряє, є чи повідомлення в черзі для даного потоку, і незалежно від результату відразу ж повертає керування. Перепишемо наш попередній приклад так:
void breakTask(GF_Task* tsk)
{
DWORD result;
char s[512];
// команда потоку, що знімається, на зняття
tsk->putState(tsBreak,True);
// збільшуємо його відносний пріоритет
// до максимально можливого
SetThreadPriority(tsk->TaskHnd95,
THREAD_PRIORITY_TIME_CRITICAL)
int cnt = 1000/20;
// чекаємо завершення потоку протягом приблизно 1 з
while(cnt-)
{ // стимулюємо Windows до передачі кванта часу
// іншим потокам
PeekMessage(&_Msg,0,0,0,PM_NOREMOVE);
// чекаємо завершення потоку
result = WaitForSingleObject(tsk->TaskHnd95,20);
// якщо все-таки не дочекалися,
// те виходимо з циклу чекання
if(result!= WAIT_TIMEOUT)
break;
}
...і т.д.
}
У документації по SDK затверджується, що для передачі кванта часу іншим потокам можна викликати функцію Sleep з параметром 0 (Sleep(0)). Тому в літературі по системному програмуванню рекомендують для стимуляції Windows до передачі кванта часу синхронізувати потоки, використовуючи функцію PeekMessage.
Лістинг 1. Обмеження доступу до масиву з використанням критичних розділів
// Масив значень.
int mas[1000];
// Семафор, що регулює доступ до критичного розділу.
CRITICAL_SECTION CritSec;
{
...
// Инициализируем семафор критичного розділу.
InitializeCriticalSection(&CritSect);
... // Текст програми.
// Видаляємо об'єкт критичного розділу.
DeleteCriticalSection(&CritSec);
}
// Перший потік: запис у масив даних.
DWORD thread1(LPVOID par)
{ // Запис значення в масив.
// Запит на вхід у критичний розділ.
EnterCriticalSection(&CritSec);
// Виконання коду в критичному розділі.
for(int i = 0;i<1000;i++)
{
mas[i] = i;
}
// Вихід із критичного розділу:
// звільняємо критичний розділ для доступу
// до нього інших задач.
LeaveCriticalSection(&CritSec);
return 0;
}
// Другий потік: зчитування даних з масиву.
DWORD thread2(LPVOID par)
{ // Зчитування значень з масиву.
int j;
// Запит на вхід у критичний розділ.
EnterCriticalSection(&CritSec);
// Виконання коду в критичному розділі.
for(int i = 0;i<1000;i++)
{
j = mas[i];
}
// Вихід із критичного розділу:
// звільняємо критичний розділ для доступу
// до нього інших задач.
LeaveCriticalSection(&CritSec);
return 0;
}
Лістинг 2. Обмеження доступу до масиву з використанням семафорів, що виключають
// Масив значень.
int mas[1000];
// Об'єкт, що регулює доступ до поділюваного коду.
HANDLE CritMutex;
{
...
// Инициализируем семафор поділюваного коду.
CritMutex = SectCreateMutex(NULL,FALSE,NULL);
... // Текст програми.
// Закриваємо об'єкт доступу до поділюваного коду.
CloseHandle(CritMutex);
}
// Перший потік: запис у масив даних.
DWORD thread1(LPVOID par)
{ // Запис значень у масив.
// Запит на вхід у захищений розділ.
DWORD dw = WaitForSingleObject(CritMutex,INFINITE);
if(dw == WAIT_OBJECT_0)
{ // Якщо об'єкт звільнений коректно, те
// виконання коду в захищеному розділі.
for(int i = 0;i<1000;i++)
{
mas[i] = i;
}
// Вихід із захищеного розділу:
// звільняємо об'єкт для доступу
// до захищеного розділу інших задач.
ReleaseMutex(CritMutex);
}
return 0;
}
// Другий потік: зчитування даних з масиву.
DWORD thread2(LPVOID par)
{ // Зчитування значень з масиву.
int j;
// Запит на вхід у захищений розділ.
DWORD dw = WaitForSingleObject(CritMutex,INFINITE);
if(dw == WAIT_OBJECT_0)
{ // Якщо об'єкт звільнений коректно, те
// виконання коду в захищеному розділі.
for(int i = 0;i<1000;i++)
{
j = mas[i];
}
// Вихід із захищеного розділу:
// звільняємо об'єкт для доступу
// до захищеного розділу інших задач.
ReleaseMutex(CritMutex);
}
return 0;
}
Лістинг 3. Зняття графічного потоку
void breakTask(GF_Task* tsk)
{
DWORD result;
char s[512];
// Команда задачі, що знімається, на зняття.
tsk->putState(tsBreak,True);
// Чекаємо завершення потоку протягом 1 с.
WaitForSingleObject(tsk->TaskHnd95,1000);
//
// Аналіз відповіді.
//
if(result == WAIT_OBJECT_0) // Ok - потік довершений успішно.
{
result = cmOK;
goto _L_EndbreakTask;
}
else if(result == WAIT_TIMEOUT) // Потік не відповідає.
{ // Підготовляємо рядок запиту.
sprintf(s,,
"Потік # %і не відповідає...nобїект %sn Зробіть вибір: n
'Так' - повторити команду на зняття n
'Немає' - зняти потік примусово n
'Скасувати' - не знімати потік"
TaskCollection->indexOf(tsk)+1,
tsk->getName());
}
// Висновок запиту на екран.
result = MsgBox(s, msg|msgSound);
switch(result) // Аналіз відповіді.
{
case cmNo: // Примусове зняття потоку.
tsk->putState(tsCrash,True); // Виставляємо прапор
tsk->endTask(); // Заключні операції
TerminateThread(tsk->TaskHnd95,0); // Знімаємо потік
goto _L_EndbreakTask;
case cmCancel: // Скасування зняття потоку.
goto _L_EndbreakTask;
}
}
else if(WAIT_FAILED) // Відбулася помилка доступу до об'єкта.
{ // Примусове зняття потоку.
SIL(); // Звуковий сигнал
tsk->endTask(); // Заключні операції
TerminateThread(tsk->TaskHnd95,0);
SIL(); // Звуковий сигнал
result = cmNo;
goto _L_EndbreakTask;
}
}
_L_EndbreakTask:
CloseHandle(tsk->TaskHnd95);
tsk->TaskHnd95 = 0;
tsk->putState(tsWorkTask,False); // Знімаємо прапори
return result;
}
// Код потоку, що знімається, приблизно наступний:
DWORD thread1(LPVOID par)
{
while((state & tsBreak) == 0)
{ // Поки прапор tsBreak не виставлений, виконуємо потік.
draw() // Щось виводимо на екран.
}
return 0;
}
Завдання на виконання
Використовуючи компілятор С++, або Assembler реалізувати програму синхронізації згідно варіанту. Під час роботи програми треба весь час виводити інформацію про те як працюють потоки (у файл, або реалізувати графічну візуалізацію). Якщо у Вашому завданні мова іде про розподіл ресурсів між потоками треба фіксувати звільнення і зайняття ресурсів потоками. При використанні механізму подій та семафорів треба фіксувати переходи у вільні стани (події) та переходи у сигнальний стан (таймери).
Написати з використанням подій програму, що реалізують таку схему за допомогою подій. Нехай є клієнт та сервер, які повинні спілкуватись між собою. Спочатку сервер (це один потік) просто чекає. Клієнт (другий потік) приймає у користувача повідомлення та передає його серверу. Сервер обробляє повідомлення, а саме: він формує нове повідомлення яке складається з n повторів початкового повідомлення, де n – кількість символів у початковому повідомленні. Весь цей час клієнт чекає, потім отримує відповідь і виводить її користувачу.
Виконати завдання №1 використовуючи механізм таймерів. (треба виставити достатній час для обробки запитів.)
Написати програму, що має два потока; один готує дані (наприклад зчитує з файла), а інший відсилає їх на сервер. Треба розпаралелити їх роботу. Тут потоки повинні працювати по черзі. Спочатку перший потік готує порцію даних. Потім другий потік відправляє її, а перший тим часом готує наступну порцію і т.д. (для такої синхронізації потрібно буде дві події “автосбросом”).
Написати з використанням м’ютекса програму, що створює десять процесів, кожен з яких у свою чергу створює десять потоків, у кожному з потоків треба підрахувати значення факторіалу сумі свого номеру (a) та номеру свого процесу (b), потім занести номер свого процесу (1-10), свій номер (1-10), та обчислене значення у файл (вхід у стек). Після цього кожен потік повинен обчислити значення ступеню a^b. Знову, аналогічним чином відмітитись у файлі та, якщо можна, залишити відмітку про вихід зі стеку. Якщо вийти зі стеку неможливо чекати і потім вийти. Таким чином кожен потік пише у файл два рази або один раз. Переконатись у тому, що є потоки, які не чекають своєї черги вийти зі стеку. (Стек повинен спрацювати за стандартним правилом: перший зайшов – останній вийшов.)
Виконати завдання №4 використовуючи механізм подій (детально продумайте схему, прийдеться використовувати досить багато подій).
Виконати завдання №4 використовуючи критичні секції.
Виконати завдання №4 використовуючи семафор.
Виконати завдання №4 з тією різницею, що потоки повинні створити чергу. (Черга працює за правилом FIFO. Перший зайшов – перший вийшов. Поки не вийдуть всі потоки перед данним, він повинен чекати).
Виконати завдання №5 з тією різницею, що потоки повинні створити чергу. (Черга працює за правилом FIFO. Перший зайшов – перший вийшов. Поки не вийдуть всі потоки перед данним, він повинен чекати).
Виконати завдання №6 з тією різницею, що потоки повинні створити чергу. (Черга працює за правилом FIFO. Перший зайшов – перший вийшов. Поки не вийдуть всі потоки перед данним, він повинен чекати).
Виконати завдання №7 з тією різницею, що потоки повинні створити чергу. (Черга працює за правилом FIFO. Перший зайшов – перший вийшов. Поки не вийдуть всі потоки перед данним, він повинен чекати).
Написати програму (використовувати м’ютекс та семафор), що створює 5 клієнтських та 2 серверних потока. Для кожного потока зафіксувати деякий період часу, який він чекає, а потім поміщає у чергу свій номер та номер свого запиту (1,2 і т.д.). Серверні потоки нічого не роблять поки у черзі не з’явиться хоча б один єлемент. Як тільки він з’явився один з серверних потоків обробляє його (пише у файл свій номер та отриманий запит). Після обробки потік “засинає” на фіксований час. Якщо в черзі більше 10 запитів програма зупиняється.
Підібрати такі значення часових проміжків, щоб черга переповнювалась.
Написати програму (використовуючи критичні секції та семафор), що аналогічна до програми з завдання №12. Кількість клієнтів - 2, кількість серверів – 4.
Використовуючи подію з ручним сбросом реалізувати наступне: Ваша програма повинна створити два потока. Один пише у файл (можна працювати просто з пам’ятю) 1000 одиниць і потім у циклі читає цей файл та змінює 1 на 0 та навпаки. (Після другої «прогонки» у файлі будуть лише нулю, потім тільки одиниці і т.д.). Другий потік повинен «просинатися» через фіксовані проміжки часу та підраховувати кількість одиниць у файлі.
За допомогою м’ютекса та таймера реалізувати наступне: Один потік просинається через однакові проміжки часу та додає 1 за модулем десять до числа у файлі (спочатку там нуль). Ваша програма повинна змоделювати будильник, що пікає кожну годину таке число разів, скільки записано у файлі.
Виконати завдання №15 використовуючи критичні секції та таймера.
Виконати завдання №15 використовуючи семафори та таймери.
Використовуючи подію і таймер написати програму, що створює два потока, що «працюють» по черзі (так само як у завданні №1, але потоком не треба обмінюватись повідомленнями). Ваша програма повинна моделювати будильник, який кожну годину дивиться який з процесів працює (№1 або №2) та пікає 1 або 2 рази відповідно.
За критичної секції та подій написати програму, що створює 11 потоків. Кожен з 10 перших потоків підраховує числа 10^10, 20^9, 30^8, 40^8, 50^7, 60^6, 70^5, 80^4, 90^2, 100^1 відповідно. Їх роботу необхідно синхронізувати таким чином, щоб вони по черзі писали результат у файл, а одинадцятий потік послідовно зчитував ці числа та додавав їх.
Написати програму з завдання №19 із використанням семафорів та подій.
Завдання на виконання
Використовуючи компілятор С++, або Assembler реалізувати програму синхронізації згідно варіанту:
1.Перевірити твердження SDK про те, що потоки мають дисципліну захоплення мютекса за правилом FIFO
2.Перевірити порядок виконання потоків після сигналізації події(event)
3. Написати бібліотеку роботи з комплексними числами в багатопоточному середовищі за допомогою критичної секції (одне блокування на всі потоки)
4.Написати бібліотеку роботи з комплексними числами в багатопоточному середовищі за допомогою повязаного з комплексним числом м’ютекса
5.Написати бібліотеку роботи з комплексними числами в багатопоточному середовищі за допомогою пов‘язаного з комплексним числом мютекса(окремо читання і для запису)
6. Написати бібліотеку роботи з векторами в багатопоточному середовищі за допомогою критичної секції (одне блокування на всі потоки)
7.Написати бібліотеку роботи з векторами в багатопоточному середовищі за допомогою повязаного з вектором м’ютекса
8.Написати бібліотеку роботи з векторами в багатопоточному середовищі за допомогою пов’язаного з вектором м’ютекса(окремо читання і для запису)
9.З‘ясувати дисципліну виконання потоків, які стояли в черзі по мірі звільнення семафора
10.Організувати синхронізацію між іменованими створеними обєктами за рахунок подій(event)
11.Написати бібліотеку, яка узагальнює поняття критичної секції на міжпроцесній взаємодії
12.Написати бібліотеку роботи зі стеком в багатопоточному середовищі за допомогою повязаного з вектором м’ютекса
13.Написати бібліотеку роботи зі стеком в багатопоточному середовищі за допомогою критичної секції (одне блокування на всі потоки)
14.Написати бібліотеку роботи зі стеком в багатопоточному середовищі за допомогою пов’язаного з вектором м’ютекса(окремо читання і для запису)