Міністерство освіти, науки, молоді та спорту України
Національний університет “Львівська політехніка”
Кафедра ЕОМ
Звіт
до лабораторної роботи №2
з дисципліни: «Системне програмне забезпечення»
на тему: «Взаємодія між потоками»
Львів 2015
Мета роботи: засвоїти поняття паралельного виконання «потоків» та освоїти засоби їх синхронізації. Здобути навики синхронізації «потоків» при обробці спільних даних та доступу до ресурсів в операційній системі Windows.
Короткі теоретичні відомості:
Windows надає чотири об’єкти, призначені для синхронізації потоків і процесів. Три з них — мютекси, семафори і події — є об’єктами ядра, що мають дескриптори. Події використовуються також для інших цілей, наприклад, для асинхронного уведення-виведення.
Спочатку розглянемо четвертий об’єкт, а саме, об’єкт критичної ділянки коду CRITICAL_SECTION. Через простоту і продуктивність, об’єктам критичних ділянок коду надається перевага, якщо їх можливостей достатньо для того, щоб задовольнити вимоги програміста.
1.1. Об'єкти критичних ділянок коду
Об’єкт критичної ділянки коду — це ділянка програмного коду, який кожного разу повинен виконуватися тільки одним потоком; паралельне виконання цієї ділянки декількома потоками може приводити до непередбачуваних або невірних результатів.
Об’єкти CRITICAL_SECTION (CS) можна ініціалізувати і видаляти, але вони не мають дескрипторів і не можуть спільно використовуватися іншими процесами. Відповідні змінні повинні оголошуватися як змінні типу CRITICAL_SECTION. Потоки входять в об’єкти CS і покидають їх, але виконання коду окремого об’єкту CS кожного разу дозволено тільки одному потоку. Разом з тим, один і той же потік може входити в декілька окремих об’єктів CS і покидати їх, якщо вони розташовані в різних місцях програми.
Для ініціалізації і видалення змінної типу CRITICAL_SECTION використовуються, відповідно, функції InitializeCriticalSection() і DeleteCritical-Section():
VOID InitializeCriticalSection(
LPCRITICAL_SECTION ipCriticalSection)
VOID DeleteCriticalSection(
LPCRITICAL_SECTION IpCriticalSection}
Функція EnterCriticalSection() блокує потік, якщо на даній критичній ділянці коду присутній інший потік. Потік, що очікує розблокуєся після того, як інший потік виконає функцію LeaveCriticalSection(). Говорять, що потік отримав права володіння об’єктом CS, якщо відбулося повернення з функції EnterCriticalSection(), тоді як для того щоб віддати права володіння використовується функція LeaveCriticalSection(). Завжди необхідно стежити за своєчасною переуступкою прав володіння об’єктами CS; недотримання цього правила може привести до того, що інші потоки перебуватимуть в стані очікування протягом невизначеного часу навіть після завершення виконання потоку-власника.
VOID EnterCriticalSection (
LPCRITICAL_SECTION IpCriticalSection)
VOID LeaveCriticalSection (
LPCRITICAL_SECTION IpCriticalSection)
Потік, що володіє об'єктом CS, може повторно увійти до цього ж CS без його блокування: це означає, що об’єкти CRITICAL_SECIION є рекурсивними (recursive). Підтримується лічильник входжень в об’єкт CS, і тому потік повинен покинути дану CS стільки раз, скільки було входжень в неї, щоб розблокувати цей об’єкт для інших потоків. Ця можливість може виявитися корисною для реалізації рекурсивних функцій і забезпечення безпечного багатопотокового виконання функцій загальних бібліотек.
Вихід з, об’єкту CS, яким даний потік не володіє, може призвести до непередбачуваних результатів, включаючи блокування самого потоку.
Для повернення з функції EnterCriticalSection() не існує кінцевого інтервалу очікування; інші потоки будуть блоковані на невизначений час, поки потік, що володіє об’єктом CS, не покине його. Проте, використовуючи функцію TryEnterCriticalSection(), можна тестувати (опитати) CS, щоб перевірити, чи не володіє ним інший потік.
BOOL TryEnterCriticalSection (
LPCRITICAL_SECTION ipCriticalSection)
Повернення функцією TryEnterCriticalSection() значення true означає, що цей потік придбав права володіння критичною ділянкою коду, тоді як повернення значення false говорить про те, що дана критична ділянка коду вже належить іншому потокові.
Об’єкти CRITICAL_SECTION мають ту перевагу, що вони не є об’єктами ядра і підтримуються в призначеному для користувача просторі. Звичайно, але не завжди, це приводить до додаткового поліпшення показників продуктивності.
1.2. Мютекси
Об’єкт взаємного виключення (mutual exception), або мютекс (mutex), забезпечує більш універсальну функціональність в порівнянні з об’єктом CRITICAL_SECTION. Оскільки мютекси можуть мати імена і дескриптори, їх можна використовувати також для синхронізації потоків, що належать різним процесам. Так, два процеси, що розділяють спільну пам’ять за допомогою відображення файлів, можуть використовувати мютекси для синхронізації доступу до областей пам’яті, що розділяються.
Об’єкти мютексів аналогічні об’єктам CS, проте, додатково до можливості їх сумісного використовування різними процесами, вони допускають кінцеві періоди очікування, а мютекси, що покинуті (abandoned) процесом який завершився переходять в сигнальний стан. Потік придбаває права володіння мютексом (або блокує (block) мютекс) шляхом виклику функції очікування (WaitForSingleObject() або WaitForMultipleObjects()) по відношенню до дескриптора мютексу і поступається цими правами за допомогою виклику функції ReleaseMutex().
Як завжди, необхідно ретельно стежити за тим, щоб потоки своєчасно звільняли ресурси, в яких вони більше не мають потреби. Потік може заволодівати одним і тим же ресурсом кілька разів, і при цьому не блокуватиметься навіть в тих випадках, коли вже володіє даним ресурсом. Кінець кінцем, потік повинен звільнити мютекс стільки раз, скільки він його захоплювала. При роботі з мютексами використовуються функції CreateMutex(), ReleaseMutex() і OpenMutex().
HANDLE CreateMutex (LPSECURITY_ATTRIBUTES lpsa
BOOL blnitialOwner, LPCTSTR lpMutexName)
BOOL ReleaseMutex (HANDLE hMutex)
blnitialOwner — якщо значення цього прапора встановлено рівним true, потік негайно придбаває права володіння новим мютексом. Ця атомарна операція дозволяє запобігти придбанню прав володіння мютексом іншими потоками, перш ніж це зробить потік, що створив мьютекс. Як випливає з самого нього назви (initial owner — початковий власник), цей прапор не надає ніякої дії, якщо мютекс вже існує. lpMutexName — покажчик на рядок, що містить ім’я мютексу; на відміну від файлів імена мютексів чутливі до регістру. Якщо цей параметр рівний NULL, то мьютекс створюється без імені. Події, мютекси, семафори, відображення файлів і інші об’єкти ядра — використовують один і той же простір імен, відмінний від простору імен файлової системи. Тому імена всіх об’єктів синхронізації повинні бути різними. Довжина вказаних імен не може перевищувати 260 символів.
Значення, що повертається, має тип HANDLE; значення NULL указує на невдале завершення функції.
Функція OpenMutex() відкриває існуючий іменований мютекс. Ця функція дає можливість потокам, що належать різним процесам, синхронізуватися так, як якби вони належали одному і тому ж процесу. Виклику функції OpenMutex() в одному процесі повинен передувати виклик функції CreateMutex() в іншому процесі. При виклику функції OpenMutex() завжди передбачається, що спочатку один процес, наприклад сервер, викликає функцію Create() для створення іменованого об’єкту, а потім інші процеси викликають функцію Open(), яка завершується невдачею, якщо іменований об’єкт до цього моменту ще не був створений. Можливий і такий варіант, коли всі процеси самостійно використовують виклик функції Create() з одним і тим же ім’ям, якщо порядок створення об’єктів не має значення.
Функція ReleaseMutex() звільняє мютекс, яким володіє цей потік. Якщо мютекс не належить потоку, функція завершується з помилкою.
BOOL ReleaseMutex (HANDLE hMutex)
Мютекс, яким потік володів та завершившись не звільнив його, називають покинутим (abandoned), і його дескриптор переходить в сигнальний стан. На те, що дескриптор (дескриптори) представляє покинутий мютекс (мютекси), указує повернення функцією WaitForSingleObject() значення WAIT_ABANDONED_0.
Те, що дескриптори покинутих мютексів переходять в сигнальний стан, є вельми корисною їх властивістю, недоступною для об’єктів CS. Виявлення покинутого мютексу може означати наявність дефекту в коді, що організовує роботу потоків, оскільки потоки повинні програмуватися так, щоб ресурси завжди звільнялися, перш ніж потік завершить своє виконання. Можливо також, що виконання даного потоку було перервано іншим потоком.
1.3. Семафори
Об’єкти другого з трьох згаданих на початку типів об’єктів синхронізації ядра — семафори (semaphores), підтримують лічильники, і коли значення цього лічильника більше 0, об’єкт семафора знаходиться в сигнальному стані. Якщо ж значення лічильника стає нульовим, об’єкт семафора переходить в несигнальний стан.
Потоки і процеси організовують очікування звичайним способом, використовуючи для цього одну або декілька функцій очікування.
До функцій управління семафорами відносяться CreateSemaphore(), QpenSemaphore() і ReleaseSemaphore(), причому остання функція може збільшити начення лічильника на 1 і більше. Ці функції аналогічні своїм еквівалентам, призначеним для управління мютексими.
HANDLE CreateSemaphore (LPSECURITY^ATTRIBUTES lpsa LONG lSemlnitial, LONG lSemMax, LPCTSTR lpSemName)
Параметр lSemMax, значення якого повинне бути рівним, принаймні 1, визначає максимально допустиме значення лічильника семафора. Параметр lSemlnitial — початкове значення цього лічильника, яке повинне задовольняти наступній умові: 0 < lSemlnitial < lSemMax і ніколи не повинно виходити за межі вказаного діапазону. Повернення функцією значення NULL указує на її невдале виконання.
Кожна окрема операція очікування може зменшити значення лічильника тільки на 1, але за допомогою функції ReleaseSemaphore() значення його лічильника може бути збільшено до будь-якого значення аж до максимально допустимого.
BOOL ReleaseSemaphore ( HANDLE hSemaphore LONG cReleaseCount, LPLONG lpPreviousCount)
Зверніть увагу на можливість отримання попереднього значення лічильника, що визначається покажчиком lpPreviousCount, яке він мав до звільнення об’єкту синхронізації за допомогою функції ReleaseSemaphore(), але якщо необхідності в цьому нема, то значення згаданого покажчика слід встановити рівним NULL.
Число, що додається до лічильника семафора (cReleaseCount), повинне бути більше 0, але якщо виконання функції ReleaseSemaphore() приводить до виходу значення лічильника за межі допустимого діапазону, то вона завершується з помилкою, повертаючи значення FALSE, а значення лічильника семафора залишається незмінним. Попереднім значенням лічильника слід користуватися з обережністю, оскільки воно могло бути змінено іншими потоками. Крім того, неможливо визначити, чи досяг лічильник максимально допустимого значення, оскільки не був передбачений засіб, що відстежує збільшення лічильника в результаті його звільнення.
Як не спокусливо намагатися розглядати мютекс як окремий випадок семафора, значення лічильника якого було задано рівним 1, це було б помилкою зважаючи на відсутність поняття прав володіння семафором. Семафор може бути звільнений будь-яким потоком, а не тільки тим, який чекає. Так само, оскільки не можна говорити про права володіння семафором, відсутнє і поняття покинутого семафора.
Класичною областю вживання семафорів є управління розподілом кінцевих ресурсів, коли значення лічильника семафора асоціюється з певною кількістю доступних ресурсів, наприклад, кількістю повідомлень, що знаходяться в черзі. Тоді максимальне значення лічильника відповідає максимальному розміру черги. Таким чином, виробник поміщає повідомлення в буфер і викликає функцію ReleaseSemaphore(), звичайно із збільшенням значення лічильника на 1 (cReleaseCount). Потоки споживача чекатимуть переходу семафора в сигнальний стан, одержуючи повідомлення і зменшуючи значення лічильника.
Не дивлячись на всі зручності, які обіцяє використовування семафорів, вони є в деякому розумінні зайвими в тому значенні, що мютекси і події, за умови їх сумісного використовування, пропонують набагато більш широкі можливості, ніж семафори.
Хід виконання роботи:
Варіант завдання:
Системнийвиклик
Варіант
3
Critical Section
3
Mutex
2
Semaphores
Текст програми 1:
#include<windows.h>
#include<stdio.h>
#include<conio.h>
CRITICAL_SECTION CS;
void WINAPI OutputThreadCS( LPVOID );
void CallCriticalSection();
int main()
{
printf( "Lab4 - Process:\n" );
printf( "Enter 'c' - CRITICAL_SECTION, 's' - to stop... \n" );
char action = getch(); //для керування клавішами
system( "cls"); //для очищення екрана консолі
static HANDLE hProcess = NULL; //тут буде зберігаєтися дескриптор відкритого процесу
static STARTUPINFO StInfoProcess; //тут містяться дані про відкриття процесу
static PROCESS_INFORMATION ProcessInfo; ;//тут зберігаються дані про процес
switch (action)
{
case'c':
printf( "CRITICAL_SECTION\n" );
CallCriticalSection(); //виклик функції
break;
case's':
break;
default:
printf( "ERROR! There is no action with '%c'. Choose another one...", action );
}
printf( "\n" );
printf( "Press any key to continue...");
getch();
system( "cls" );
return 0;
}
void CallCriticalSection()
{
HANDLE aThread[3];
DWORD ThreadID;
InitializeCriticalSection(&CS);// ініціалізовуємо змінну типу CRITICAL_SECTION
for(int i=0; i < 3; i++)// цикл створення потоків
{
aThread[i] = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE) OutputThreadCS,0,0,&ThreadID);
if( aThread[i] == NULL )
{
printf("CreateThread error: %d\n", GetLastError());//вилучає значеня коду останньої помилки потоку
}
}
Sleep(100);//чекаємо
while (!TryEnterCriticalSection(&CS))//поки введена критична секція чекаємо
Sleep(100);
DeleteCriticalSection(&CS);//видаляємо змінну типу CRITICAL_SECTION
}
void WINAPI OutputThreadCS( LPVOID lpParam )
{
DWORD dwCount=0;
EnterCriticalSection(&CS);//потік отримав права володіння об’єктом CS
while ( dwCount < 5 )
{
printf("ThreadID: %5d\n", GetCurrentThreadId()); // вилучає значення ідентифікатора викликаючого потоку
dwCount++;
}
LeaveCriticalSection(&CS);//віддає права володіння
}
Текст програми 2:
#include<windows.h>
#include<stdio.h>
#include<conio.h>
HANDLE hMutex;
DWORD WINAPI OutputThreadMutex( LPVOID );
void CallMutex();
int main()
{
printf( "Lab4 - Process:\n" );
printf( "Enter 'c' - Mutex, 's' - to stop... \n" );
char action = getch(); //для керування клавішами
system( "cls"); //для очищення екрана консолі
static HANDLE hProcess = NULL; //тут буде зберігаєтися дескриптор відкритого процесу
static STARTUPINFO StInfoProcess; //тут містяться дані про відкриття процесу
static PROCESS_INFORMATION ProcessInfo; ;//тут зберігаються дані про процес
switch (action)
{
case'c':
printf( "Mutex\n" );
CallMutex(); //виклик функції
break;
case's':
break;
default:
printf( "ERROR! There is no action with '%c'. Choose another one...", action );
}
printf( "Press any key to continue...");
getch();
system( "cls" );
return 0;
}
void CallMutex()
{
HANDLE aThread[4];
DWORD ThreadID;
int i;
hMutex = CreateMutex(NULL,FALSE,NULL); //створюємо м'ютекс
if (hMutex == NULL)
{
printf("Create Mutex error: %d\n", GetLastError());//вилучає значеня коду останньої помилки
}
for( i=0; i < 4; i++ )
{
aThread[i] = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE) OutputThreadMutex,
NULL,0,&ThreadID);//створення потоків
if( aThread[i] == NULL )
{
printf("CreateThreaderror: %d\n", GetLastError());//вилучає значеня коду останньої помилки
}
}
WaitForMultipleObjects(3, aThread, TRUE, INFINITE);//придбання прав володіння, функція очікування
for( i=0; i < 4; i++ )//
CloseHandle(aThread[i]);//закриває дискриптор відкритого об'єкта
CloseHandle(hMutex);////закриває дискриптор відкритого об'єкта
ReleaseMutex(hMutex);//втрачає права володіння потоком
}
DWORD WINAPI OutputThreadMutex( LPVOID lpParam )
{
DWORD dwCount=0, dwWaitResult;
while( dwCount< 5)
{
dwWaitResult = WaitForSingleObject(hMutex,INFINITE); //придбання прав володiння, функція очікування
switch (dwWaitResult)
{
case WAIT_OBJECT_0:
__try { printf("ThreadID: %5d\n", GetCurrentThreadId());//вилучає значення ідентифікатора викликаючого потоку
dwCount++;}
__finally {
if (! ReleaseMutex(hMutex)) // якщо об'єкт не звільнений
{ printf("Handl eerror\n");}
}
break;
case WAIT_ABANDONED:
return FALSE;
}
} return TRUE;
}
Результати виконання програми:
1) CriticalSection:
//
2) Mutex:
//
Висновок: виконавши цю лабораторну роботу, я засвоїв поняття паралельного виконання «потоків» та засоби їх синхронізації. Здобув навики синхронізації «потоків» при обробці спільних даних та доступу до ресурсів в операційній системі Windows.