Логотип коледжу
Оптико-механічний фаховий коледж

Лабораторна робота №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 з назвою роботи та командами компіляції.

Питання для захисту

  1. Що таке стан гонитви і чому результат race_demo не визначений за стандартом C?
  2. Яку вимогу коректної синхронізації забезпечує м'ютекс?
  3. Хто має розблокувати POSIX mutex після pthread_mutex_lock?
  4. Чим лічильний семафор відрізняється від м'ютекса?
  5. Що станеться, якщо забути викликати sem_post у гілці після критичної секції?

Пов'язані лекції

Ресурси

Здати роботу