Лабораторна робота №6. Синхронізація потоків м'ютексами та семафорами
Мета
Відтворити стан гонитви між потоками, захистити критичну секцію за допомогою pthread_mutex, а також дослідити лічильний семафор POSIX як засіб обмеження одночасного доступу.
Підготовка
Перед виконанням опрацюйте пов'язані лекції:
- Стан гонитви, критична секція та атомарні операції
- М'ютекси, семафори й класичні задачі синхронізації
Для роботи потрібне Linux-середовище з GCC. Усі програми компілюються з прапором -pthread. Робота виконується в межах папки lab-06.
Важливо про стан гонитви. Програма з незахищеним доступом до спільного лічильника у C є прикладом undefined behavior за стандартом мови: результат не гарантований і може відрізнятися між системами. Завдання 1 демонструє явище в навчальних цілях, але реальний код не повинен покладатися на таку поведінку.
Завдання
Завдання 1. Відтворіть стан гонитви
У папці lab-06 створіть файл race_demo.c:
#include <pthread.h>
#include <stdio.h>
#define THREADS 4
#define ITERS 1000000
static int counter = 0;
void *worker(void *arg) {
(void)arg;
for (int i = 0; i < ITERS; i++) {
counter++;
}
return NULL;
}
int main(void) {
pthread_t threads[THREADS];
for (int i = 0; i < THREADS; i++) {
pthread_create(&threads[i], NULL, worker, NULL);
}
for (int i = 0; i < THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("Очікувано: %d\n", THREADS * ITERS);
printf("Отримано: %d\n", counter);
return 0;
}
Скомпілюйте та запустіть кілька разів:
gcc -Wall -Wextra -pedantic -pthread -o race_demo race_demo.c
./race_demo
./race_demo
./race_demo
Запишіть отримані значення. Поясніть, чому результат зазвичай менший за очікуваний 4000000, і чому на деяких системах може збігатися з ним (це не «правильність», а наслідок того, що поведінка незахищеного доступу не визначена).
Завдання 2. Захистіть критичну секцію м'ютексом
Створіть файл race_fixed.c на основі race_demo.c, додавши м'ютекс:
#include <pthread.h>
#include <stdio.h>
#define THREADS 4
#define ITERS 1000000
static int counter = 0;
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *worker(void *arg) {
(void)arg;
for (int i = 0; i < ITERS; i++) {
pthread_mutex_lock(&lock);
counter++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main(void) {
pthread_t threads[THREADS];
for (int i = 0; i < THREADS; i++) {
pthread_create(&threads[i], NULL, worker, NULL);
}
for (int i = 0; i < THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("Очікувано: %d\n", THREADS * ITERS);
printf("Отримано: %d\n", counter);
return 0;
}
Скомпілюйте та запустіть кілька разів:
gcc -Wall -Wextra -pedantic -pthread -o race_fixed race_fixed.c
./race_fixed
Переконайтеся, що тепер «Отримано» завжди дорівнює 4000000. Поясніть, яку частину коду захищає м'ютекс і чому counter++ став «неподільним» між двома операціями pthread_mutex_lock/pthread_mutex_unlock.
Завдання 3. Прикладний: лічильний семафор
Створіть файл semaphore_demo.c, у якому семафор обмежує кількість потоків, що одночасно перебувають у критичній секції:
#include <pthread.h>
#include <semaphore.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#define THREADS 4
#define MAX_ACTIVE 2
static sem_t gate;
static int active = 0;
static int max_active = 0;
static pthread_mutex_t stat_lock = PTHREAD_MUTEX_INITIALIZER;
void *worker(void *arg) {
long id = (long)(uintptr_t)arg;
sem_wait(&gate);
int cur;
pthread_mutex_lock(&stat_lock);
active++;
if (active > max_active) max_active = active;
cur = active;
pthread_mutex_unlock(&stat_lock);
printf("Потік %ld увійшов, активних: %d\n", id, cur);
usleep(50000);
pthread_mutex_lock(&stat_lock);
active--;
pthread_mutex_unlock(&stat_lock);
sem_post(&gate);
return NULL;
}
int main(void) {
pthread_t threads[THREADS];
sem_init(&gate, 0, MAX_ACTIVE);
for (long i = 0; i < THREADS; i++) {
pthread_create(&threads[i], NULL, worker, (void *)(uintptr_t)i);
}
for (long i = 0; i < THREADS; i++) {
pthread_join(threads[i], NULL);
}
sem_destroy(&gate);
printf("Максимальна кількість одночасних активних: %d (межа %d)\n",
max_active, MAX_ACTIVE);
return 0;
}
Скомпілюйте та запустіть:
gcc -Wall -Wextra -pedantic -pthread -o semaphore_demo semaphore_demo.c
./semaphore_demo
Поясніть, чому кількість одночасних активних потоків ніколи не перевищує 2, попри те що потоків чотири. Потім змініть MAX_ACTIVE на 1 і на 4, запустіть обидва варіанти та поясніть, чим семафор зі значенням 1 схожий на м'ютекс.
Очікуваний результат
Для завдання 1 «Отримано» зазвичай менше за 4000000 (точне значення нестабільне). Для завдання 2 обидва рядки:
Очікувано: 4000000
Отримано: 4000000
Для завдання 3 з MAX_ACTIVE = 2 вивід має завершуватися рядком:
Максимальна кількість одночасних активних: 2 (межа 2)
Порядок рядків «Потік N увійшов...» може змінюватися між запусками.
Структура репозиторію
operational-systems/
├── README.md
├── lab-01/
├── lab-02/
├── lab-03/
├── lab-04/
├── lab-05/
└── lab-06/
├── race_demo.c
├── race_fixed.c
└── semaphore_demo.c
Що здати
- Посилання на ваш особистий репозиторій
operational-systems, надіслане у формі здачі на поточній сторінці. - Папку
lab-06з файламиrace_demo.c,race_fixed.cіsemaphore_demo.c. - Оновлений кореневий
README.mdз назвою роботи та командами компіляції.
Питання для захисту
- Що таке стан гонитви і чому результат
race_demoне визначений за стандартом C? - Яку вимогу коректної синхронізації забезпечує м'ютекс?
- Хто має розблокувати POSIX mutex після
pthread_mutex_lock? - Чим лічильний семафор відрізняється від м'ютекса?
- Що станеться, якщо забути викликати
sem_postу гілці після критичної секції?
