Лабораторна робота № 2
Тема: «Взаємодія між потоками»
Мета: Засвоїти поняття паралельного виконання «потоків» та освоїти засоби їх синхронізації. Здобути навики синхронізації «потоків» при обробці спільних даних та доступу до ресурсів в операційній системі Windows.
1. Теоретична частина
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). Потоки споживача чекатимуть переходу семафора в сигнальний стан, одержуючи повідомлення і зменшуючи значення лічильника.
Не дивлячись на всі зручності, які обіцяє використовування семафорів, вони є в деякому розумінні зайвими в тому значенні, що мютекси і події, за умови їх сумісного використовування, пропонують набагато більш широкі можливості, ніж семафори.
2. Завдання на лабораторну роботу
1. Дослідити роботу програми в середовищі Visual Studio, що демонструє використання об’єктів критичних ділянок коду та приведена нижче.
/* Maintain two threads, a producer and a consumer */
/* The producer periodically creates checksummed data buffers, */
/* or "message block" which the consumer displays when prompted */
/* by the user. The conusmer reads the most recent complete */
/* set of data and validates it before display */
#include "EvryThng.h"
#include <time.h>
#define DATA_SIZE 256
typedef struct msg_block_tag { /* Message block */
volatile DWORD f_ready, f_stop;
/* ready state flag, stop flag */
volatile DWORD sequence; /* Message block sequence number */
volatile DWORD nCons, nLost;
time_t timestamp;
CRITICAL_SECTION mguard; /* Guard the message block structure */
DWORD checksum; /* Message contents checksum */
DWORD data[DATA_SIZE]; /* Message Contents */
} MSG_BLOCK;
/* One of the following conditions holds for the message block */
/* 1) !f_ready || f_stop */
/* nothing is assured about the data OR */
/* 2) f_ready && data is valid */
/* && checksum and timestamp are valid */
/* Also, at all times, 0 <= nLost + nCons <= sequence */
/* Single message block, ready to fill with a new message */
MSG_BLOCK mblock = { 0, 0, 0, 0, 0 };
DWORD WINAPI produce (void *);
DWORD WINAPI consume (void *);
void MessageFill (MSG_BLOCK *);
void MessageDisplay (MSG_BLOCK *);

DWORD _tmain (DWORD argc, LPTSTR argv[])
{
DWORD Status, ThId;
HANDLE produce_h, consume_h;

/* Initialize the message block CRITICAL SECTION */
InitializeCriticalSection (&mblock.mguard);
/* Create the two threads */
produce_h = (HANDLE)_beginthreadex (NULL, 0, produce, NULL, 0, &ThId);
if (produce_h == NULL)
ReportError (_T("Cannot create producer thread"), 1, TRUE);
consume_h = (HANDLE)_beginthreadex (NULL, 0, consume, NULL, 0, &ThId);
if (consume_h == NULL)
ReportError (_T("Cannot create consumer thread"), 2, TRUE);

/* Wait for the producer and consumer to complete */

Status = WaitForSingleObject (consume_h, INFINITE);
if (Status != WAIT_OBJECT_0)
ReportError (_T("Failed waiting for consumer thread"), 3, TRUE);
Status = WaitForSingleObject (produce_h, INFINITE);
if (Status != WAIT_OBJECT_0)
ReportError (__T("Failed waiting for producer thread"), 4, TRUE);
DeleteCriticalSection (&mblock.mguard);
_tprintf (_T("Producer and consumer threads have terminated\n"));
_tprintf (_T("Messages produced: %d, Consumed: %d, Known Lost: %d\n"),
mblock.sequence, mblock.nCons, mblock.nLost);
return 0;
}
DWORD WINAPI produce (void *arg)
/* Producer thread - Create new messages at random intervals */
{

srand ((DWORD)time(NULL)); /* Seed the random # generator */

while (!mblock.f_stop) {
/* Random Delay */
Sleep(rand()/100);

/* Get the buffer, fill it */

EnterCriticalSection (&mblock.mguard);
__try {
if (!mblock.f_stop) {
mblock.f_ready = 0;
MessageFill (&mblock);
mblock.f_ready = 1;
mblock.sequence++;
}
}
__finally { LeaveCriticalSection (&mblock.mguard); }
}
return 0;
}
DWORD WINAPI consume (void *arg)
{
DWORD ShutDown = 0;
CHAR command, extra;
/* Consume the NEXT message when prompted by the user */
while (!ShutDown) { /* This is the only thread accessing stdin, stdout */
_tprintf (_T("\n**Enter 'c' for consume; 's' to stop: "));
_tscanf ("%c%c", &command, &extra);
if (command == 's') {
EnterCriticalSection (&mblock.mguard);
ShutDown = mblock.f_stop = 1;
LeaveCriticalSection (&mblock.mguard);
} else if (command == 'c') { /* Get a new buffer to consume */
EnterCriticalSection (&mblock.mguard);
__try {
if (mblock.f_ready == 0)
_tprintf (_T("No new messages. Try again later\n"));
else {
MessageDisplay (&mblock);
mblock.nCons++;
mblock.nLost = mblock.sequence - mblock.nCons;
mblock.f_ready = 0; /* No new messages are ready */
}
}
__finally { LeaveCriticalSection (&mblock.mguard); }
} else {
_tprintf (_T("Illegal command. Try again.\n"));
}
}
return 0;
}
void MessageFill (MSG_BLOCK *mblock)
{
/* Fill the message buffer, and include checksum and timestamp */
/* This function is called from the producer thread while it */
/* owns the message block mutex */

DWORD i;

mblock->checksum = 0;
for (i = 0; i < DATA_SIZE; i++) {
mblock->data[i] = rand();
mblock->checksum ^= mblock->data[i];
}
mblock->timestamp = time(NULL);
return;
}
void MessageDisplay (MSG_BLOCK *mblock)
{
/* Display message buffer, timestamp, and validate checksum */
/* This function is called from the consumer thread while it */
/* owns the message block mutex */
DWORD i, tcheck = 0;

for (i = 0; i < DATA_SIZE; i++)
tcheck ^= mblock->data[i];
_tprintf (_T("\nMessage number %d generated at: %s"),
mblock->sequence, _tctime (&(mblock->timestamp)));
_tprintf (_T("First and last entries: %x %x\n"),
mblock->data[0], mblock->data[DATA_SIZE-1]);
if (tcheck == mblock->checksum)
_tprintf (_T("GOOD ->Checksum was validated.\n"));
else
_tprintf (_T("BAD ->Checksum failed. message was corrupted\n"));

return;
}
2. Відповідно до варіанту (таблиця) модифікувати програму так, щоб замінити об’єкти синхронізації заданого числа потоків
Таблиця – Варіанти завдань
Системнийвиклик
Варіант


1
2
3
4
5
6
7
8
9
10

Critical Section
2
2
3
3
4
2
2
3
3
4

Mutex
2
3
2
4
3






Semaphores





2
3
2
4
3


3. Проаналізувати та пояснити вміст дисплею після завершення програми.