Практическая работа №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 (Взаимная блокировка): Ситуация, когда два или более потока блокируют друг друга, ожидая ресурсы, которые заняты другими потоками из этой же группы. В результате ни один из потоков не может продолжить выполнение.
- Непредсказуемый порядок выполнения: Из-за особенностей планирования потоков операционной системой, порядок выполнения операций в разных потоках не гарантирован, что усложняет отладку и тестирование.
1.3. Мьютексы (<mutex>)
Section titled “1.3. Мьютексы (<mutex>)”Мьютекс (от англ. 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;}1.4. Семафоры (<semaphore>)
Section titled “1.4. Семафоры (<semaphore>)”Семафор — это механизм синхронизации, который управляет доступом к ограниченному количеству ресурсов. В отличие от мьютекса, который предоставляет эксклюзивный доступ (только один поток), семафор позволяет определенному количеству потоков одновременно получать доступ к ресурсу. Семафор имеет внутренний счетчик, который уменьшается при захвате ресурса (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.5 данной практической работы.
- Выполнить общее задание: Реализовать модель «Поликлиника» согласно описанию, используя мьютексы и семафоры для синхронизации.
- Выполнить задание по варианту: Выбрать один из предложенных вариантов и реализовать его, применяя полученные знания о многопоточности, мьютексах и семафорах.
- Сдать работу преподавателю и ответить на вопросы.
3. Общее задание
Section titled “3. Общее задание”Модель: «Поликлиника»
Section titled “Модель: «Поликлиника»”Описание:
В поликлинике имеется 3 кабинета, которые могут обслуживать пациентов. Всего в поликлинику приходят 8 пациентов (потоков). Каждый пациент проходит следующие этапы:
- Приходит: Пациент появляется в поликлинике.
- Ждёт: Пациент ожидает в очереди, если все кабинеты заняты.
- Обслуживается: Пациент заходит в свободный кабинет и получает обслуживание.
- Уходит: Пациент покидает поликлинику.
Требования к реализации:
- Использовать семафор для ограничения одновременного доступа к кабинетам (3 кабинета).
- Использовать мьютекс для защиты вывода в консоль (лога) и/или для защиты общей переменной, если таковая будет использоваться (например, счетчик обслуженных пациентов).
- Реализовать задержку (
std::this_thread::sleep_for) для имитации времени ожидания и обслуживания. - Выводить лог событий в консоль, отражающий действия каждого пациента (пришел, ждет, обслуживается, ушел). Лог должен быть синхронизирован, чтобы сообщения не перемешивались.
4. Задания по вариантам
Section titled “4. Задания по вариантам”Выберите один из следующих вариантов и реализуйте его, используя принципы многопоточности, мьютексы и семафоры. Обязательно предусмотрите логирование событий и имитацию задержек.
Вариант 1 - Парковка
Section titled “Вариант 1 - Парковка”Описание:
Реализовать модель парковки с ограниченным количеством мест. Автомобили (потоки) приезжают на парковку, ищут свободное место, паркуются, стоят некоторое время, затем уезжают.
Требования:
- Ограничение на количество парковочных мест (например, 3 места).
- Использование семафора для управления доступом к парковочным местам.
- Использование мьютекса для защиты вывода логов.
- Имитация ожидания свободного места и времени стоянки.
- Логирование событий: “Автомобиль X приехал”, “Автомобиль X ждет место”, “Автомобиль X припарковался на месте Y”, “Автомобиль X уехал”.
Вариант 2 - Банк
Section titled “Вариант 2 - Банк”Описание:
Модель работы банка с несколькими кассирами и очередью клиентов. Клиенты (потоки) приходят в банк, встают в очередь, обслуживаются у кассира и уходят.
Требования:
- Ограничение на количество одновременно обслуживаемых клиентов (например, 2 кассира).
- Использование семафора для управления доступом к кассирам.
- Использование мьютекса для защиты вывода логов и/или для защиты общей очереди клиентов.
- Имитация времени обслуживания и ожидания в очереди.
- Логирование событий: “Клиент X пришел”, “Клиент X встал в очередь”, “Клиент X обслуживается у кассира Y”, “Клиент X ушел”.
Вариант 3 - Лифт
Section titled “Вариант 3 - Лифт”Описание:
Модель работы лифта, который перевозит пассажиров между этажами. Лифт имеет ограниченную вместимость. Пассажиры (потоки) вызывают лифт, заходят в него, едут на нужный этаж и выходят.
Требования:
- Ограничение на количество пассажиров в лифте (например, 4 человека).
- Использование семафора для управления вместимостью лифта.
- Использование мьютекса для защиты вывода логов и/или для управления состоянием лифта (текущий этаж, направление).
- Имитация времени движения лифта и посадки/высадки пассажиров.
- Логирование событий: “Пассажир X вызвал лифт на этаже Y”, “Лифт прибыл на этаж Y”, “Пассажир X зашел в лифт”, “Лифт едет на этаж Z”, “Пассажир X вышел на этаже Z”.
Вариант 4 - Автомойка
Section titled “Вариант 4 - Автомойка”Описание:
Модель автомойки с несколькими постами. Автомобили (потоки) приезжают на автомойку, ждут свободный пост, моются и уезжают.
Требования:
- Ограничение на количество постов для мойки (например, 2 поста).
- Использование семафора для управления доступом к постам.
- Использование мьютекса для защиты вывода логов.
- Имитация ожидания и времени мойки.
- Логирование событий: “Автомобиль X приехал”, “Автомобиль X ждет пост”, “Автомобиль X моется на посту Y”, “Автомобиль X уехал”.
Вариант 5 - Склад
Section titled “Вариант 5 - Склад”Описание:
Модель склада, на котором работают несколько грузчиков (потоков), выполняющих операции по загрузке и выгрузке товаров. Склад имеет ограниченную вместимость.
Требования:
- Ограничение на количество одновременно работающих грузчиков (например, 3 грузчика).
- Использование семафора для управления доступом грузчиков к складу.
- Использование мьютекса для защиты вывода логов и/или для защиты состояния склада (количество товаров).
- Имитация времени выполнения операций.
- Логирование событий: “Грузчик X начал работу”, “Грузчик X загружает товар”, “Грузчик X выгружает товар”, “Грузчик X закончил работу”.
Вариант 6 - Кафе
Section titled “Вариант 6 - Кафе”Описание:
Модель кафе с ограниченным количеством столиков. Посетители (потоки) приходят в кафе, ждут свободный столик, делают заказ, едят и уходят.
Требования:
- Ограничение на количество столиков (например, 4 столика).
- Использование семафора для управления доступом к столикам.
- Использование мьютекса для защиты вывода логов и/или для защиты состояния столиков.
- Имитация ожидания и времени обслуживания.
- Логирование событий: “Посетитель X пришел”, “Посетитель X ждет столик”, “Посетитель X занял столик Y”, “Посетитель X ест”, “Посетитель X ушел”.
Вариант 7 - Поликлиника
Section titled “Вариант 7 - Поликлиника”Описание:
Расширенная модель поликлиники. Добавить к общему заданию возможность выбора специализации кабинетов или приоритета пациентов.
Требования:
- Использовать семафор для ограничения доступа к кабинетам.
- Использовать мьютекс для защиты вывода логов.
- Реализовать дополнительную логику (например, разные типы кабинетов или приоритетные пациенты).
- Логирование событий с учетом новой логики.
Вариант 8 - Кинотеатр
Section titled “Вариант 8 - Кинотеатр”Описание:
Модель кинотеатра с ограниченным количеством мест в зале. Зрители (потоки) приходят в кинотеатр, покупают билеты, занимают места в зале, смотрят фильм и уходят.
Требования:
- Ограничение на количество мест в зале (например, 10 мест).
- Использование семафора для управления доступом к местам.
- Использование мьютекса для защиты вывода логов и/или для защиты состояния мест.
- Имитация времени просмотра фильма.
- Логирование событий: “Зритель X пришел”, “Зритель X купил билет”, “Зритель X занял место Y”, “Зритель X смотрит фильм”, “Зритель X ушел”.
Вариант 9 - Мойка
Section titled “Вариант 9 - Мойка”Описание:
Модель мойки с несколькими постами для разных типов транспорта (например, легковые и грузовые автомобили). Транспорт (потоки) приезжает на мойку, ждет свободный пост соответствующего типа, моется и уезжает.
Требования:
- Разделение постов по типам транспорта (например, 2 поста для легковых, 1 для грузовых).
- Использование нескольких семафоров для управления доступом к разным типам постов.
- Использование мьютекса для защиты вывода логов.
- Имитация ожидания и времени мойки.
- Логирование событий с учетом типов транспорта и постов.
Вариант 10 - Гостиница
Section titled “Вариант 10 - Гостиница”Описание:
Модель гостиницы с ограниченным количеством номеров. Гости (потоки) приезжают в гостиницу, регистрируются, заселяются в свободный номер, живут некоторое время и выезжают.
Требования:
- Ограничение на количество номеров (например, 5 номеров).
- Использование семафора для управления доступом к номерам.
- Использование мьютекса для защиты вывода логов и/или для защиты состояния номеров.
- Имитация времени проживания.
- Логирование событий: “Гость X приехал”, “Гость X регистрируется”, “Гость X заселился в номер Y”, “Гость X выехал из номера Y”.
Вариант 11 - Автовокзал
Section titled “Вариант 11 - Автовокзал”Описание:
Модель автовокзала, где автобусы (потоки) прибывают на платформы, высаживают и забирают пассажиров, затем отправляются. Количество платформ ограничено.
Требования:
- Ограничение на количество платформ (например, 2 платформы).
- Использование семафора для управления доступом к платформам.
- Использование мьютекса для защиты вывода логов и/или для защиты состояния платформ.
- Имитация времени посадки/высадки пассажиров.
- Логирование событий: “Автобус X прибыл на платформу Y”, “Автобус X высаживает/забирает пассажиров”, “Автобус X отправился с платформы Y”.
Вариант 12 - Спортзал
Section titled “Вариант 12 - Спортзал”Описание:
Модель спортзала с ограниченным количеством тренажеров. Посетители (потоки) приходят в спортзал, занимают свободный тренажер, тренируются и уходят.
Требования:
- Ограничение на количество тренажеров (например, 5 тренажеров).
- Использование семафора для управления доступом к тренажерам.
- Использование мьютекса для защиты вывода логов.
- Имитация времени тренировки.
- Логирование событий: “Посетитель X пришел”, “Посетитель X ждет тренажер”, “Посетитель X занял тренажер Y”, “Посетитель X тренируется”, “Посетитель X ушел”.
Вариант 13 - Серверная
Section titled “Вариант 13 - Серверная”Описание:
Модель серверной комнаты с ограниченным количеством стоек для серверов. Инженеры (потоки) приходят в серверную, получают доступ к стойке, выполняют работы и уходят.
Требования:
- Ограничение на количество доступных стоек (например, 3 стойки).
- Использование семафора для управления доступом к стойкам.
- Использование мьютекса для защиты вывода логов и/или для защиты состояния стоек.
- Имитация времени выполнения работ.
- Логирование событий: “Инженер X пришел”, “Инженер X ждет доступ к стойке”, “Инженер X работает со стойкой Y”, “Инженер X закончил работу и ушел”.
Вариант 14 - Библиотека
Section titled “Вариант 14 - Библиотека”Описание:
Модель библиотеки с ограниченным количеством читальных мест. Читатели (потоки) приходят в библиотеку, занимают свободное читальное место, читают книги и уходят.
Требования:
- Ограничение на количество читальных мест (например, 6 мест).
- Использование семафора для управления доступом к читальным местам.
- Использование мьютекса для защиты вывода логов.
- Имитация времени чтения.
- Логирование событий: “Читатель X пришел”, “Читатель X ждет место”, “Читатель X занял место Y”, “Читатель X читает”, “Читатель X ушел”.
Вариант 15 - Почта
Section titled “Вариант 15 - Почта”Описание:
Модель почтового отделения с несколькими окнами обслуживания. Клиенты (потоки) приходят на почту, ждут свободное окно, обслуживаются и уходят.
Требования:
- Ограничение на количество окон обслуживания (например, 3 окна).
- Использование семафора для управления доступом к окнам.
- Использование мьютекса для защиты вывода логов и/или для защиты очереди клиентов.
- Имитация ожидания и времени обслуживания.
- Логирование событий: “Клиент X пришел”, “Клиент X ждет окно”, “Клиент X обслуживается в окне Y”, “Клиент X ушел”.
5. Контрольные вопросы
Section titled “5. Контрольные вопросы”-
Что такое поток (thread) в контексте многопоточного программирования?
- Объясните, чем поток отличается от процесса.
- Каковы преимущества использования потоков?
- Как создать и запустить поток в C++ с использованием
std::thread? - В чем разница между
join()иdetach()?
-
Что такое мьютекс (mutex) и для чего он используется?
- Объясните принцип работы мьютекса.
- Приведите примеры проблем, которые решает мьютекс.
- Как использовать
std::mutexв C++? - В чем преимущества использования
std::lock_guardиstd::unique_lockпо сравнению с прямым вызовомlock()/unlock()?
-
Что такое семафор (semaphore) и для чего он используется?
- Объясните принцип работы семафора.
- Приведите примеры проблем, которые решает семафор.
- Как использовать
std::counting_semaphoreв C++? - Что такое бинарный семафор и чем он отличается от
std::mutex?
-
В чем заключаются основные различия между мьютексом и семафором?
- Сравните их по назначению, механизму работы, владению и типичным сценариям использования.
- Приведите примеры ситуаций, когда предпочтительнее использовать мьютекс, а когда — семафор.
-
Что такое состояние гонки (race condition) и как его избежать?
- Объясните, почему возникает состояние гонки.
- Приведите пример кода, демонстрирующий состояние гонки.
- Какие механизмы синхронизации помогают предотвратить состояние гонки?
-
Что такое взаимная блокировка (deadlock) и как ее предотвратить?
- Объясните условия возникновения deadlock.
- Приведите пример сценария, который может привести к deadlock.
- Какие стратегии используются для предотвращения или обнаружения deadlock?