Skip to content

Практическая работа №20. Работа с семафорами и мьютексами

Цель работы: Освоить применение мьютексов и семафоров для синхронизации потоков, научиться предотвращать состояние гонки и корректно работать с общими ресурсами.


1. Основные теоретические положения

Section titled “1. Основные теоретические положения”

1.1. Многопоточность в C++

Section titled “1.1. Многопоточность в C++”

Многопоточность позволяет выполнять несколько задач параллельно в рамках одного процесса, что может значительно повысить производительность и отзывчивость приложений. В C++ для работы с потоками используется стандартная библиотека <thread>, появившаяся в C++11. Каждый объект std::thread представляет собой отдельный поток выполнения.

Пример создания и запуска потока:

#include <iostream>
#include <thread>
#include <chrono> // Для std::chrono::seconds и std::this_thread::sleep_for
void thread_function() {
std::cout << "Поток запущен!\n";
std::this_thread::sleep_for(std::chrono::seconds(1)); // Имитация работы
std::cout << "Поток завершен!\n";
}
int main() {
std::cout << "Основной поток: Запуск дочернего потока.\n";
std::thread t(thread_function); // Создание и запуск потока
// `join()` блокирует основной поток до завершения дочернего потока.
// Это необходимо для того, чтобы основной поток не завершился раньше дочернего,
// что может привести к неопределенному поведению.
t.join();
std::cout << "Основной поток: Дочерний поток завершен.\n";
return 0;
}

Важные моменты:

  • std::thread::join(): Ожидает завершения выполнения потока. Если join() не вызван, а поток уничтожается, программа завершится с ошибкой std::terminate.
  • std::thread::detach(): Отсоединяет поток от объекта std::thread. Отсоединенный поток продолжает выполняться в фоновом режиме, и его завершение не может быть отслежено через std::thread объект. Ресурсы отсоединенного потока освобождаются автоматически после его завершения.

1.2. Основные проблемы многопоточности

Section titled “1.2. Основные проблемы многопоточности”

При параллельном выполнении задач возникают специфические проблемы, которые необходимо учитывать для корректной работы программы:

  • Состояние гонки (Race Condition): Возникает, когда несколько потоков одновременно пытаются получить доступ к общим данным, и результат выполнения зависит от порядка их выполнения. Это может привести к непредсказуемым и некорректным результатам.
  • Нарушение целостности данных: Прямое следствие состояния гонки, когда одновременный доступ к общим данным приводит к их повреждению или несогласованному состоянию.
  • Deadlock (Взаимная блокировка): Ситуация, когда два или более потока блокируют друг друга, ожидая ресурсы, которые заняты другими потоками из этой же группы. В результате ни один из потоков не может продолжить выполнение.
  • Непредсказуемый порядок выполнения: Из-за особенностей планирования потоков операционной системой, порядок выполнения операций в разных потоках не гарантирован, что усложняет отладку и тестирование.

Мьютекс (от англ. Mutual Exclusion — взаимное исключение) — это механизм синхронизации, который обеспечивает эксклюзивный доступ к общему ресурсу. Только один поток может владеть мьютексом в любой момент времени. Если поток пытается заблокировать мьютекс, который уже заблокирован другим потоком, он будет ждать, пока мьютекс не будет освобожден.

В C++ мьютексы реализованы в заголовке <mutex>. Основные типы мьютексов:

  • std::mutex: Простой мьютекс, нерекурсивный.
  • std::recursive_mutex: Рекурсивный мьютекс, позволяющий одному и тому же потоку блокировать его несколько раз.
  • std::timed_mutex: Мьютекс с возможностью установки таймаута для блокировки.
  • std::recursive_timed_mutex: Рекурсивный мьютекс с таймаутом.

Пример использования std::mutex для защиты критической секции:

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx; // Объявление мьютекса
int shared_data = 0;
void increment_shared_data() {
for (int i = 0; i < 100000; ++i) {
mtx.lock(); // Блокировка мьютекса перед доступом к общей переменной
shared_data++;
mtx.unlock(); // Разблокировка мьютекса после завершения работы с общей переменной
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment_shared_data);
}
for (std::thread& t : threads) {
t.join();
}
std::cout << "Значение shared_data: " << shared_data << " (Ожидалось: 1000000)\n";
return 0;
}

В данном примере без мьютекса shared_data почти наверняка не достигнет значения 1000000 из-за состояния гонки. Мьютекс гарантирует, что только один поток изменяет shared_data в любой момент времени.

Рекомендуемый способ использования std::mutex — RAII-обертки:

Для безопасного использования мьютексов и автоматической разблокировки даже в случае исключений рекомендуется использовать RAII (Resource Acquisition Is Initialization) обертки, такие как std::lock_guard или std::unique_lock.

  • std::lock_guard<std::mutex> lock(mtx);: Блокирует мьютекс при создании объекта и автоматически разблокирует его при выходе из области видимости. Прост в использовании, но не предоставляет гибкости (например, нельзя разблокировать мьютекс досрочно).
  • std::unique_lock<std::mutex> lock(mtx);: Более гибкая обертка, позволяющая вручную блокировать/разблокировать мьютекс, а также использовать его с условными переменными (std::condition_variable).

Пример с std::lock_guard:

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx;
int shared_data_guard = 0;
void increment_shared_data_guard() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // Мьютекс автоматически блокируется и разблокируется
shared_data_guard++;
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment_shared_data_guard);
}
for (std::thread& t : threads) {
t.join();
}
std::cout << "Значение shared_data_guard: " << shared_data_guard << " (Ожидалось: 1000000)\n";
return 0;
}

Семафор — это механизм синхронизации, который управляет доступом к ограниченному количеству ресурсов. В отличие от мьютекса, который предоставляет эксклюзивный доступ (только один поток), семафор позволяет определенному количеству потоков одновременно получать доступ к ресурсу. Семафор имеет внутренний счетчик, который уменьшается при захвате ресурса (acquire или wait) и увеличивается при его освобождении (release или post).

В C++20 появился стандартный заголовок <semaphore>, предоставляющий std::counting_semaphore и std::binary_semaphore.

  • std::counting_semaphore<LeastMaxValue>: Семафор со счетчиком, который может принимать значения от 0 до LeastMaxValue.
  • std::binary_semaphore: Бинарный семафор, эквивалентный мьютексу (счетчик 0 или 1).

Пример использования std::counting_semaphore:

Представим, что у нас есть 3 принтера, и 5 потоков хотят печатать. Семафор может ограничить одновременный доступ к принтерам.

#include <iostream>
#include <thread>
#include <semaphore>
#include <vector>
#include <chrono>
// Семафор, позволяющий 3 потокам одновременно получать доступ к ресурсу
std::counting_semaphore<3> printer_semaphore(3);
void print_task(int id) {
std::cout << "Поток " << id << ": Ожидает доступ к принтеру.\n";
printer_semaphore.acquire(); // Захват ресурса (уменьшение счетчика)
std::cout << "Поток " << id << ": Печатает...\n";
std::this_thread::sleep_for(std::chrono::seconds(2)); // Имитация печати
std::cout << "Поток " << id << ": Завершил печать.\n";
printer_semaphore.release(); // Освобождение ресурса (увеличение счетчика)
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(print_task, i + 1);
}
for (std::thread& t : threads) {
t.join();
}
std::cout << "Все потоки завершили работу.\n";
return 0;
}

В этом примере только 3 потока смогут одновременно выполнять print_task, остальные будут ждать своей очереди.

1.5. Различие между мьютексом и семафором

Section titled “1.5. Различие между мьютексом и семафором”

Хотя мьютексы и семафоры используются для синхронизации потоков, у них есть ключевые различия:

ХарактеристикаМьютексСемафор
НазначениеЗащита критической секции, эксклюзивный доступ к одному ресурсу.Управление доступом к ограниченному пулу ресурсов.
ВладениеДолжен быть разблокирован тем же потоком, который его заблокировал.Может быть разблокирован любым потоком.
СчетчикНет (бинарное состояние: заблокирован/разблокирован).Есть (счетчик доступных ресурсов).
Типичное использованиеЗащита общих переменных, предотвращение состояния гонки.Ограничение количества одновременно работающих потоков, управление доступом к пулу ресурсов (например, соединениям с базой данных, принтерам).

2. Порядок выполнения работы

Section titled “2. Порядок выполнения работы”
  1. Изучить теоретический материал: Внимательно ознакомиться с разделами 1.1 - 1.5 данной практической работы.
  2. Выполнить общее задание: Реализовать модель «Поликлиника» согласно описанию, используя мьютексы и семафоры для синхронизации.
  3. Выполнить задание по варианту: Выбрать один из предложенных вариантов и реализовать его, применяя полученные знания о многопоточности, мьютексах и семафорах.
  4. Сдать работу преподавателю и ответить на вопросы.

Описание:
В поликлинике имеется 3 кабинета, которые могут обслуживать пациентов. Всего в поликлинику приходят 8 пациентов (потоков). Каждый пациент проходит следующие этапы:

  1. Приходит: Пациент появляется в поликлинике.
  2. Ждёт: Пациент ожидает в очереди, если все кабинеты заняты.
  3. Обслуживается: Пациент заходит в свободный кабинет и получает обслуживание.
  4. Уходит: Пациент покидает поликлинику.

Требования к реализации:

  • Использовать семафор для ограничения одновременного доступа к кабинетам (3 кабинета).
  • Использовать мьютекс для защиты вывода в консоль (лога) и/или для защиты общей переменной, если таковая будет использоваться (например, счетчик обслуженных пациентов).
  • Реализовать задержку (std::this_thread::sleep_for) для имитации времени ожидания и обслуживания.
  • Выводить лог событий в консоль, отражающий действия каждого пациента (пришел, ждет, обслуживается, ушел). Лог должен быть синхронизирован, чтобы сообщения не перемешивались.

Выберите один из следующих вариантов и реализуйте его, используя принципы многопоточности, мьютексы и семафоры. Обязательно предусмотрите логирование событий и имитацию задержек.

Описание:
Реализовать модель парковки с ограниченным количеством мест. Автомобили (потоки) приезжают на парковку, ищут свободное место, паркуются, стоят некоторое время, затем уезжают.

Требования:

  • Ограничение на количество парковочных мест (например, 3 места).
  • Использование семафора для управления доступом к парковочным местам.
  • Использование мьютекса для защиты вывода логов.
  • Имитация ожидания свободного места и времени стоянки.
  • Логирование событий: “Автомобиль X приехал”, “Автомобиль X ждет место”, “Автомобиль X припарковался на месте Y”, “Автомобиль X уехал”.

Описание:
Модель работы банка с несколькими кассирами и очередью клиентов. Клиенты (потоки) приходят в банк, встают в очередь, обслуживаются у кассира и уходят.

Требования:

  • Ограничение на количество одновременно обслуживаемых клиентов (например, 2 кассира).
  • Использование семафора для управления доступом к кассирам.
  • Использование мьютекса для защиты вывода логов и/или для защиты общей очереди клиентов.
  • Имитация времени обслуживания и ожидания в очереди.
  • Логирование событий: “Клиент X пришел”, “Клиент X встал в очередь”, “Клиент X обслуживается у кассира Y”, “Клиент X ушел”.

Описание:
Модель работы лифта, который перевозит пассажиров между этажами. Лифт имеет ограниченную вместимость. Пассажиры (потоки) вызывают лифт, заходят в него, едут на нужный этаж и выходят.

Требования:

  • Ограничение на количество пассажиров в лифте (например, 4 человека).
  • Использование семафора для управления вместимостью лифта.
  • Использование мьютекса для защиты вывода логов и/или для управления состоянием лифта (текущий этаж, направление).
  • Имитация времени движения лифта и посадки/высадки пассажиров.
  • Логирование событий: “Пассажир X вызвал лифт на этаже Y”, “Лифт прибыл на этаж Y”, “Пассажир X зашел в лифт”, “Лифт едет на этаж Z”, “Пассажир X вышел на этаже Z”.

Описание:
Модель автомойки с несколькими постами. Автомобили (потоки) приезжают на автомойку, ждут свободный пост, моются и уезжают.

Требования:

  • Ограничение на количество постов для мойки (например, 2 поста).
  • Использование семафора для управления доступом к постам.
  • Использование мьютекса для защиты вывода логов.
  • Имитация ожидания и времени мойки.
  • Логирование событий: “Автомобиль X приехал”, “Автомобиль X ждет пост”, “Автомобиль X моется на посту Y”, “Автомобиль X уехал”.

Описание:
Модель склада, на котором работают несколько грузчиков (потоков), выполняющих операции по загрузке и выгрузке товаров. Склад имеет ограниченную вместимость.

Требования:

  • Ограничение на количество одновременно работающих грузчиков (например, 3 грузчика).
  • Использование семафора для управления доступом грузчиков к складу.
  • Использование мьютекса для защиты вывода логов и/или для защиты состояния склада (количество товаров).
  • Имитация времени выполнения операций.
  • Логирование событий: “Грузчик X начал работу”, “Грузчик X загружает товар”, “Грузчик X выгружает товар”, “Грузчик X закончил работу”.

Описание:
Модель кафе с ограниченным количеством столиков. Посетители (потоки) приходят в кафе, ждут свободный столик, делают заказ, едят и уходят.

Требования:

  • Ограничение на количество столиков (например, 4 столика).
  • Использование семафора для управления доступом к столикам.
  • Использование мьютекса для защиты вывода логов и/или для защиты состояния столиков.
  • Имитация ожидания и времени обслуживания.
  • Логирование событий: “Посетитель X пришел”, “Посетитель X ждет столик”, “Посетитель X занял столик Y”, “Посетитель X ест”, “Посетитель X ушел”.

Описание:
Расширенная модель поликлиники. Добавить к общему заданию возможность выбора специализации кабинетов или приоритета пациентов.

Требования:

  • Использовать семафор для ограничения доступа к кабинетам.
  • Использовать мьютекс для защиты вывода логов.
  • Реализовать дополнительную логику (например, разные типы кабинетов или приоритетные пациенты).
  • Логирование событий с учетом новой логики.

Описание:
Модель кинотеатра с ограниченным количеством мест в зале. Зрители (потоки) приходят в кинотеатр, покупают билеты, занимают места в зале, смотрят фильм и уходят.

Требования:

  • Ограничение на количество мест в зале (например, 10 мест).
  • Использование семафора для управления доступом к местам.
  • Использование мьютекса для защиты вывода логов и/или для защиты состояния мест.
  • Имитация времени просмотра фильма.
  • Логирование событий: “Зритель X пришел”, “Зритель X купил билет”, “Зритель X занял место Y”, “Зритель X смотрит фильм”, “Зритель X ушел”.

Описание:
Модель мойки с несколькими постами для разных типов транспорта (например, легковые и грузовые автомобили). Транспорт (потоки) приезжает на мойку, ждет свободный пост соответствующего типа, моется и уезжает.

Требования:

  • Разделение постов по типам транспорта (например, 2 поста для легковых, 1 для грузовых).
  • Использование нескольких семафоров для управления доступом к разным типам постов.
  • Использование мьютекса для защиты вывода логов.
  • Имитация ожидания и времени мойки.
  • Логирование событий с учетом типов транспорта и постов.

Описание:
Модель гостиницы с ограниченным количеством номеров. Гости (потоки) приезжают в гостиницу, регистрируются, заселяются в свободный номер, живут некоторое время и выезжают.

Требования:

  • Ограничение на количество номеров (например, 5 номеров).
  • Использование семафора для управления доступом к номерам.
  • Использование мьютекса для защиты вывода логов и/или для защиты состояния номеров.
  • Имитация времени проживания.
  • Логирование событий: “Гость X приехал”, “Гость X регистрируется”, “Гость X заселился в номер Y”, “Гость X выехал из номера Y”.

Описание:
Модель автовокзала, где автобусы (потоки) прибывают на платформы, высаживают и забирают пассажиров, затем отправляются. Количество платформ ограничено.

Требования:

  • Ограничение на количество платформ (например, 2 платформы).
  • Использование семафора для управления доступом к платформам.
  • Использование мьютекса для защиты вывода логов и/или для защиты состояния платформ.
  • Имитация времени посадки/высадки пассажиров.
  • Логирование событий: “Автобус X прибыл на платформу Y”, “Автобус X высаживает/забирает пассажиров”, “Автобус X отправился с платформы Y”.

Описание:
Модель спортзала с ограниченным количеством тренажеров. Посетители (потоки) приходят в спортзал, занимают свободный тренажер, тренируются и уходят.

Требования:

  • Ограничение на количество тренажеров (например, 5 тренажеров).
  • Использование семафора для управления доступом к тренажерам.
  • Использование мьютекса для защиты вывода логов.
  • Имитация времени тренировки.
  • Логирование событий: “Посетитель X пришел”, “Посетитель X ждет тренажер”, “Посетитель X занял тренажер Y”, “Посетитель X тренируется”, “Посетитель X ушел”.

Описание:
Модель серверной комнаты с ограниченным количеством стоек для серверов. Инженеры (потоки) приходят в серверную, получают доступ к стойке, выполняют работы и уходят.

Требования:

  • Ограничение на количество доступных стоек (например, 3 стойки).
  • Использование семафора для управления доступом к стойкам.
  • Использование мьютекса для защиты вывода логов и/или для защиты состояния стоек.
  • Имитация времени выполнения работ.
  • Логирование событий: “Инженер X пришел”, “Инженер X ждет доступ к стойке”, “Инженер X работает со стойкой Y”, “Инженер X закончил работу и ушел”.

Описание:
Модель библиотеки с ограниченным количеством читальных мест. Читатели (потоки) приходят в библиотеку, занимают свободное читальное место, читают книги и уходят.

Требования:

  • Ограничение на количество читальных мест (например, 6 мест).
  • Использование семафора для управления доступом к читальным местам.
  • Использование мьютекса для защиты вывода логов.
  • Имитация времени чтения.
  • Логирование событий: “Читатель X пришел”, “Читатель X ждет место”, “Читатель X занял место Y”, “Читатель X читает”, “Читатель X ушел”.

Описание:
Модель почтового отделения с несколькими окнами обслуживания. Клиенты (потоки) приходят на почту, ждут свободное окно, обслуживаются и уходят.

Требования:

  • Ограничение на количество окон обслуживания (например, 3 окна).
  • Использование семафора для управления доступом к окнам.
  • Использование мьютекса для защиты вывода логов и/или для защиты очереди клиентов.
  • Имитация ожидания и времени обслуживания.
  • Логирование событий: “Клиент X пришел”, “Клиент X ждет окно”, “Клиент X обслуживается в окне Y”, “Клиент X ушел”.

  1. Что такое поток (thread) в контексте многопоточного программирования?

    • Объясните, чем поток отличается от процесса.
    • Каковы преимущества использования потоков?
    • Как создать и запустить поток в C++ с использованием std::thread?
    • В чем разница между join() и detach()?
  2. Что такое мьютекс (mutex) и для чего он используется?

    • Объясните принцип работы мьютекса.
    • Приведите примеры проблем, которые решает мьютекс.
    • Как использовать std::mutex в C++?
    • В чем преимущества использования std::lock_guard и std::unique_lock по сравнению с прямым вызовом lock()/unlock()?
  3. Что такое семафор (semaphore) и для чего он используется?

    • Объясните принцип работы семафора.
    • Приведите примеры проблем, которые решает семафор.
    • Как использовать std::counting_semaphore в C++?
    • Что такое бинарный семафор и чем он отличается от std::mutex?
  4. В чем заключаются основные различия между мьютексом и семафором?

    • Сравните их по назначению, механизму работы, владению и типичным сценариям использования.
    • Приведите примеры ситуаций, когда предпочтительнее использовать мьютекс, а когда — семафор.
  5. Что такое состояние гонки (race condition) и как его избежать?

    • Объясните, почему возникает состояние гонки.
    • Приведите пример кода, демонстрирующий состояние гонки.
    • Какие механизмы синхронизации помогают предотвратить состояние гонки?
  6. Что такое взаимная блокировка (deadlock) и как ее предотвратить?

    • Объясните условия возникновения deadlock.
    • Приведите пример сценария, который может привести к deadlock.
    • Какие стратегии используются для предотвращения или обнаружения deadlock?