You are on page 1of 11

Лабораторна робота № 3.

Багатозадачність на основі потоків у


Windows. Взаємодія потоків, синхронізація їх роботи.

Мета роботи: освоєння способів взаємодії потоків і синхронізації їх


роботи, вивчення особливостей використання об'єктів синхронізації.
Постановка задачі: Розробити застосунок, що виконує:
1. Обмін інформацією між потоками через глобальні змінні.
2. Організацію чекань завершення роботи потоків.
3. Організацію послідовного доступу до загальних ресурсів за
допомогою об'єктів синхронізації:
 критичних секцій;
 мьютексов;
 семафорів.
4. Організацію сигналізації між потоками через об'єкти-події.
Алгоритм обчислень, що виконується дочірніми потоками, вибирається
індивідуально кожним студентом (можливі варіанти: сортування масивів,
генерація випадкових чисел, запис даних у файл, виведення графічних
зображень і так далі).
Зауваження! Обєкт синхронізації “м’ютекс” використовувати для
організації доступу до спільних ресурсів у потоках, які працюють у різних
процесах.

Методичні вказівки і теоретичні відомості

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

Забезпечення спільної роботи декількох потоків – важливе завдання,


яке доводиться вирішувати при розробці багатьох програм. Потоки
повинні взаємодіяти один з одним в двох основних випадках:
 спільно використовуючи ресурс, що розділяється (аби не зруйнувати
його);
 коли потрібно повідомляти інші потоки про завершення яких-небудь
операцій.
Примітив синхронізації (synchronization primitive) – це об'єкт, який
допомагає управляти багатопотоковим застосуванням. У Windows
доступні п'ять основних типів примітивів синхнонизации:
 Події (events) – створювані програмістом об'єкти, які
використовуються для сигналізації про дозвіл доступу до змінної або
процедури.
 Критичні секції (critical sections) – області коду, доступ до яких
може здійснювати лише один поток в даний момент часу.
 Мьютекси (взаємні виключення, mutexes) – об'єкти Windows,
використання яких гарантує, що лише один поток дістає доступ до
захищеної змінної або коду.
 Семафори (semaphores) нагадують взаємні виключення, проте
функціонують як лічильники, що дозволяють визначити кількість
потоків, що здійснюють доступ до захищеної змінної або коду.
 Атомарні операції API-рівня (API-level atomic operations), надані
системою Windows, дозволяють програмістові інкрементувати,
декрементувати або обмінювати вміст змінної за одну операцію.
Кожен з приведених вище примітивів синхронізації застосовується в
певних ситуаціях.
Використання взаємно блокованих операцій Win32
Найпростіші примітиви синхронізації служать для обробки або перевірки
значення однієї або двох змінних. API Win32 включає сім функцій, які
гарантовано є атомарними і безпечними для потоків, навіть за наявності
декількох процесорів. Найчастіше використовуються функції:
 InterlockedIncrement інкрементує 32-розрядну змінну і повертає нове
значення.
 InterlockedDecrement декрементує 32-розрядну змінну і повертає
нове значення.
 InterlockedExchange замінює значення 32-розрядної змінної на нове і
повертає попереднє.

Критичні секції
Критична секція (critical section) – це ділянка коду, яка повинна
використовуватися лише одним потоком одночасно. Якщо в один час два
або більше потоків намагаються здійснити доступ до критичної секції,
контроль над нею буде наданий лише одному з потоків, а всі інші будуть
блоковані (переведені в режим чекання) до тих пір, поки секція не буде
вільна.
Порівняно з іншими методами синхронізації (які будуть описані нижче)
створення критичної секції є вигідним з точки зору витрат
обчислювальних ресурсів. Проте на відміну від інших примітивів
синхронізації Windows його можна застосовувати лише в межах одного
процесу.
Критична секція вводиться змінною CRITICAL_SECTION. Цю змінну слід
ініціалізувати до вживання, до того ж вона повинна знаходитися в області
видимості для кожного потоку, що використовує її. Змінна критичної
секції не може виходити за межі зони видимості при використанні, тому
такі змінні частенько оголошуються як глобальні.
Для ініціалізації змінної CRITICAL_SECTION використовується функція
InitializeCriticalSection:
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);

Аби оволодіти критичною ділянкою, поток повинен викликати функцію


EnterCriticalSection:
EnterCriticalSection(&cs);
Якщо критична секція не використовується, вона позначається як зайнята і
поток негайно продовжує виконання. Якщо критична секція вже
використовується, поток блокується, поки ділянка не буде звільнена.
Коли поток завершує роботу із захищеною змінною або функцією,
критична секція звільняється шляхом виклику функції LeaveCriticalSection:
LeaveCriticalSection(&cs);

Wait-функції

Wait-функції дозволяють потоку у будь-який момент припинитися і чекати


звільнення якого-небудь об'єкту ядра. Зі всього сімейства цих функцій
найчастіше використовується WaitForSingleObject:
DWORD WaitForSingleObject( HANDLE hObject
DWORD dwMilliseconds);
Коли поток викликає цю функцію, перший параметр, hObject, ідентифікує
об'єкт ядра, що підтримує стани "вільний-зайнятий". Другий параметр,
dwMilliseconds, вказує, скільки часу (у мілісекундах) поток готовий чекати
звільнення об'єкту.
Повертане значення функції WaitForSingleObject вказує, чому поток знову
став планованим. Якщо функція повертає WAIT_OBJECT_0, об'єкт
вільний, а якщо WAIT_TIMEOUT — заданий час чекання (таймаут) витік.
При передачі невірного параметра (наприклад, недопустимого описувача)
WaitForSingleObject повертає WAIT_FAILED. Аби з'ясувати конкретну
причину помилки, викличте функцію GetLastError.
Функція WaitForMultipleObjects аналогічна WaitForSingleObject з тим
виключенням, що дозволяє чекати звільнення відразу декількох об'єктів
або якогось одного із списку об'єктів:
DWORD WaitForMultipleObjects(
DWORD dwCount,
CONST HANDLE* phObjects,
BOOL fWaitAll,
DWORD dwMilliseconds);
Параметр dwCount визначає кількість об'єктів ядра, що цікавлять Вас. Його
значення має бути в межах від 1 до MAXIMUM_WAIT_OBJECTS (у
заголовних файлах Windows воно визначене як 64). Параметр phObjects —
це вказівник на масив описувачів об'єктів ядра.
WaitForMultipleObjects припиняє поток і заставляє його чекати звільнення
або всіх заданих об'єктів ядра, або одного з них. Параметр fWaitAll якраз і
визначає, чого саме Ви хочете від функції. Якщо він рівний TRUE, функція
не дасть потоку відновити свою роботу, поки не звільняться всі об'єкти.

М'ютекси
Об'єкти ядра "м'ютекси" гарантують потокам взаємовиключний доступ до
єдиного ресурсу. Звідси і виникла назва цих об'єктів (mutual exclusion,
mutex). Вони містять лічильник числа користувачів, лічильник рекурсії і
змінну, в якій запам'ятовується ідентифікатор потоку. М'ютекси
поводяться точно так, як і критичні секції. Проте, якщо останні є об'єктами
режиму користувача, то м'ютекси — об'єктами ядра. Крім того, єдиний
объект- м'ютекс дозволяє синхронізувати доступ до ресурсу декількох
потоків з різних процесів; при цьому можна задати максимальний час
чекання доступу до ресурсу.
Ідентифікатор потоку визначає, який поток захопив мьютекс, а лічильник
рекурсій — скільки разів.
Для використання об'єкта-м'ютекса один з процесів повинен спочатку
створити його викликом CreateMutex:
HANDLE CreateMutex(
PSECURITY_ATTRIBUTES psa,
BOOL bInitialOwner,
PCTSTR pszName);
Параметр bInitialOwner визначає початковий стан м'ютекса. Якщо в ньому
передається FALSE (що зазвичай і буває), об'єкт-м'ютекс не належить
жодному з потоків і тому знаходиться у вільному стані. При цьому його
ідентифікатор потоку і лічильник рекурсії дорівнюють 0. Якщо ж в ньому
передається TRUE, ідентифікатор потоку, якому належить мьютекс,
прирівнюється ідентифікатору потоку, що викликає, а лічильник рекурсії
набуває значення 1. Оскільки тепер ідентифікатор потоку відмінний від 0,
м'ютекс спочатку знаходиться в зайнятому стані.
Будь-який процес може отримати свій ("процесо-залежний") описувач
існуючого об'єкту "м'ютекс", викликавши OpenMutex:
HANDLE OpenMutex(
DWORD fdwAccess,
BOOL bInheritHandle,
PCTSTR pszName);
Поток дістає доступ до ресурсу, що розділяється, викликаючи одну з Wait-
функцій і передаючи їй описувач мьютекса, який охороняє цей ресурс.
Якщо Wait-функция визначає, що в м'ютекса ідентифікатор потоку не
дорівнює 0 (м'ютекс зайнятий), то потік, що викликає переходить в стан
чекання. Коли чекання м'ютекса потоком успішно завершується, останній
дістає монопольний доступ до захищеного ресурсу. Всі останні потоки, що
намагаються звернутися до цього ресурсу, переходять в стан чекання. Коли
поток, що займає ресурс, закінчує з ним працювати, він повинен звільнити
м'ютекс викликом функції ReleaseMutex:
BOOL ReleaseMutex(HANDLE hMutex);

Семафори
Об'єкти ядра "семафор" використовуються для обліку ресурсів. Як і всі
об'єкти ядра, вони містять лічильник числа користувачів, але, крім того,
підтримують два 32-бітові значення із знаком: одне визначає максимальне
число ресурсів (контрольоване семафором), інше використовується як
лічильник поточного числа ресурсів.
Для семафорів визначені наступні правила:
 коли лічильник поточного числа ресурсів стає більше 0, семафор
переходить у вільний стан;
 якщо цей лічильник дорівнює 0, семафор зайнятий;
 система не допускає присвоєння негативних значень лічильнику
поточного числа ресурсів;
 лічильник поточного числа ресурсів не може бути більше
максимального числа ресурсів.
Не плутайте лічильник поточного числа ресурсів з лічильником числа
користувачів об'єкту-семафора.
Об'єкт ядра "семафор" створюється викликом CreateSemaphore:
HANDLE CreateSemaphore(
PSECURITY_ATTRIBUTES psa,
LONG lInitialCount,
LONG lMaximumCount,
PCTSTR pszName);
Параметр lInitialCount визначає значення лічильника поточного числа
ресурсів, а параметр lMaximumCount – значення максимального числа
ресурсів.
Поток дістає доступ до ресурсу, викликаючи одну з Wait-функцій і
передаючи їй описувач семафора, який охороняє цей ресурс. Wait-функція
перевіряє в семафора лічильник поточного числа ресурсів, якщо його
значення більше 0 (семафор вільний), зменшує значення цього лічильника
на 1, і поток, що викликає, дістає доступ до ресурсу.
Поток збільшує значення лічильника поточного числа ресурсів,
викликаючи функцію ReleaseSemaphore:
BOOL ReleaseSemaphore(
HANDLE hSem,
LONG lReleaseCount,
PLONG plPreviousCount);

Вона просто додає величину lReleaseCount до значення лічильника


поточного числа ресурсів. Зазвичай в параметрі lReleaseCount передають 1,
але це зовсім не обов'язково. Функція повертає вихідне значення
лічильника ресурсів в *plPreviousCount. Якщо Вас не цікавить це значення
(а в більшості програм так воно і є), передайте в параметрі plPreviousCount
значення NULL.

Події
Події - найпримітивніший різновид об'єктів ядра [3]. Вони містять
лічильник числа користувачів (як і всі об'єкти ядра) і дві булеві змінні:
одна повідомляє тип даного об'єкту-події, інша — його стан (вільний або
зайнятий).
Події просто повідомляють про закінчення якої-небудь операції. Об'єкти-
події бувають двох типів: із скиданням вручну (manual-reset events) і з
автоскиданням (auto-reset events). Перші дозволяють відновлювати
виконання відразу декількох потоків, що чекають, другі — лише одного.
Об'єкт ядра "подія" створюється функцією CreateEvent:
HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
BOOL bInitialState,
PCTSTR pszName);
Параметр bManualReset (булева змінна) повідомляє систему, хочете Ви
створити подію із скиданням вручну (TRUE) або з автоскиданням (FALSE).
Параметру bInitialState визначає початковий стан події — вільний (TRUE)
або зайнятий (FALSE). Після того, як система створює об'єкт подію,
CreateEvent повертає описувач події, специфічний для конкретного
процесу. Потоки з інших процесів можуть дістати доступ до цього об'єкту:
1) викликом CreateEvent з тим же параметром pszName; 2) спадкоємством
описувача; 3) вживанням функції DuplicateHandle; і 4) викликом OpenEvent
з передачею в параметрі pszName імені, співпадаючого з вказаним в
аналогічному параметрі функції CreateEvent.
Непотрібний об'єкт ядра "подія" слід, як завжди, закрити викликом
CloseHandle. Створивши об'єкт подію, Ви можете безпосередньо управляти
його станом. Аби перевести його у вільний стан, Ви викликаєте:
BOOL SetEvent(HANDLE hEvent);
А аби поміняти його на зайняте
BOOL ResetEvent(HANDLE hEvent);
Для подій з автоскиданням діє наступне правило. Коли його чекання
потоком успішно завершується, цей об'єкт автоматично скидається в
зайнятий стан. Звідси і виникла назва таких об'єктів-подій. Для цього
об'єкту зазвичай не потрібно викликати ResetEvent, оскільки система сама
відновлює його стан. А для подій із скиданням вручну жодних побічних
ефектів успішного чекання не передбачено.
Для повноти картини згадаємо про ще одну функцію, яку можна
використовувати з об'єктами-подіями:
BOOL PulseEvent(HANDLE hEvent);
PulseEvent звільняє подію і тут же переводить її назад в зайнятий стан; її
виклик рівнозначний послідовному виклику SetEvent і ResetEvent. Якщо
Ви викликаєте PulseEvent для події із скиданням вручну, будь-які потоки,
що чекають цей об'єкт, стають планованими. При виклику цієї функції
стосовно події з автоскиданням прокидається лише один з потоків, що
чекають.

Приклад використання критичної сенкції


CRITICAL_SECTION cs;

DWORD WINAPI thread(LPVOID) {


for (int j = 0; j < 10; ++j) {
EnterCriticalSection (&cs); // входимо до критичної секції
for (int i = 0; i < 10; ++i)
{
cout << j << ' '<< flush;
Sleep(7);
}
cout << endl;
LeaveCriticalSection(&cs); } // виходимо з критичної секції
return 0; }

int main()
{
HANDLE hThread;
DWORD IDThread;
InitializeCriticalSection(&cs); // иніціалізуємо критичну секцію
hThread=CreateThread(NULL, 0, thread, NULL, 0, &IDThread);
if (hThread == NULL) return GetLastError();
for (int j = 10; j < 20; ++j)
{
EnterCriticalSection(&cs); // входимо у критичну секцію
for (int i = 0; i < 10; ++i)
{
cout << j << ' ' << flush;
Sleep(7);
}
cout << endl;
LeaveCriticalSection(&cs); // виходимо з критичної секції
}
WaitForSingleObject(hThread,INFINITE); // чекаємо, поки потік
// завершить свою роботу
DeleteCriticalSection(&cs); // закриваємо критичну секцію
return 0;
}

Приклад використання м’ютексу

Лістинг коду процесу 1


int main() {
HANDLE hMutex;
char lpszAppName[] = "C:\\ConsoleProcess.exe";
STARTUPINFO si;
PROCESS_INFORMATION pi;
hMutex = CreateMutex(NULL,FALSE,"DemoMutex"); // створюємо м’ютекс
if (hMutex == NULL) {
cout<<"Create mutex failed."<<endl;
cout<<"Press any key to exit."<<endl; cin.get();
return GetLastError(); }
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);

// створюємо новый консольный процесс


if
(!CreateProcess(lpszAppName,NULL,NULL,NULL,FALSE,NULL,NULL,NULL,&si,
&pi)){
cout << "The new process is not created." << endl;
cout << "Press any key to exit." << endl; cin.get();
return GetLastError(); }
for (int j = 0; j < 10; ++j) { // виводимо на екран рядки
WaitForSingleObject(hMutex, INFINITE); // захоплюємо м’ютекс
for (int i = 0; i < 10; i++)
{
cout << j << ' ' << flush;
Sleep(10);
}
cout << endl;
ReleaseMutex(hMutex); } // звільнюємо м’ютекс
CloseHandle(hMutex); // закриваємо дескриптор м’ютексу
WaitForSingleObject(pi.hProcess, INFINITE); // чекаємо поки
// дочірній процес завершить роботу
CloseHandle(pi.hThread); // закриваємо дескриптори дочірнього
// процесу в поточному процесі
CloseHandle(pi.hProcess);
return 0;
}

Лістинг коду процесу 2


int main()
{
HANDLE hMutex;
int i,j;

// відкриваємо м’ютекс
hMutex = OpenMutex(SYNCHRONIZE, FALSE, "DemoMutex");
if (hMutex == NULL) {
cout << "Open mutex failed." << endl;
cout << "Press any key to exit." << endl; cin.get();
return GetLastError();
}

for (j = 10; j < 20; j++)


{
WaitForSingleObject(hMutex, INFINITE); // захоплюємо м’ютекс
for (i = 0; i < 10; i++)
{
cout << j << ' ' << flush;
Sleep(5);
}
cout << endl;
ReleaseMutex(hMutex); // звільнюємо м’ютекс
}
CloseHandle(hMutex); // закриваємо дескриптор об’єкту
return 0;
}

Приклад використання семафору


int a[10];
HANDLE hSemaphore;

DWORD WINAPI thread(LPVOID)


{
for (int i = 0; i < 10; i++) {
a[i] = i + 1;
ReleaseSemaphore(hSemaphore,1,NULL); // відмічаємо, що один
Sleep(500); } // елемент готовий
return 0;
}

int main()
{
HANDLE hThread;
DWORD IDThread;
cout << "An initial state of the array: ";
for (int i = 0; i < 10; i++) cout << a[i] <<' '; cout << endl;
// створюємо семафор
hSemaphore=CreateSemaphore(NULL, 0, 10, NULL);
if (hSemaphore == NULL) return GetLastError();
// створюємо поток, який готує елементи масиву
hThread = CreateThread(NULL, 0, thread, NULL, 0, &IDThread);
if (hThread == NULL) return GetLastError();

cout << "A final state of the array: "; // потік main виводить
for (int i = 0; i < 10; i++) // елементи масиву тільки
{ // після їх підготовки
WaitForSingleObject(hSemaphore, INFINITE); // потоком thread
cout << a[i] << " \a" << flush;
}
CloseHandle(hSemaphore);
CloseHandle(hThread);
return 0;
}

Приклад використання події з автоматичним скиданням


HANDLE hOutEvent, hAddEvent;

DWORD WINAPI thread(LPVOID)


{
for (int i = 0; i < 10; ++i)
{ // виконуємо якусь корисну роботу
if (i == 4) {
SetEvent(hOutEvent);
WaitForSingleObject(hAddEvent, INFINITE); }
}
return 0;
}

int main()
{
HANDLE hThread;
DWORD IDThread;

// створюємо події з автоматичним скиданням


hOutEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
hAddEvent = CreateEvent(NULL,FALSE,FALSE,NULL);

// створюємо потік
hThread = CreateThread(NULL,0,thread,NULL,0,&IDThread);

// чекаємо, поки потік виконає половину роботи


WaitForSingleObject(hOutEvent,INFINITE);
cout<<"A half of the work is done."<<endl; cin.get();

SetEvent(hAddEvent); // дозволяємо далі працювати потоку thread

WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hOutEvent);
CloseHandle(hAddEvent);

cout << "The work is done." << endl;


return 0;
}

Контрольні запитання

1. Які засоби синхронізації потоків Ви знаєте?


2. В яких випадках у багатопоточних застосунках необхідно
синхронізовувати роботу потоків?
3. Які з засобів синхронізації можуть бути використані для
синхронізації потоків, що працюють в різних процесах?
4. Опишіть принцип роботи критичної секції?
5. Опишіть принцип роботи м’ютексу?
6. Опишіть принцип роботи семафору?
7. Опишіть принцип роботи подій?
8. Навіщо використовуються атомарні операції?

You might also like