Сначала пройдусь по определениям.
1. Что такое синхронизация и зачем она нужна?
Очевидно, что набор действий мы можем выполнять несколькими способами. Самые простые — последовательно и параллельно. Параллельности выполнения определенных действий можно достигнуть за счет запуска различных потоков (threads). Идея простая: назначаем каждому потоку какое-то элементарное (или не очень) действие и запускаем их в определенном порядке. Вообще говоря, запустить мы их можем и все одновременно — выигрыш по времени мы, конечно, получим. Это понятно: одно дело вывести 10 000 слов одно за другим, а другое дело одновременно выводить, например, 100 слов. 100-кратный выигрыш по времени (плюс-минус, без учета задержек и проч.). Но исходная задача может предполагать строгую последовательность действий.
Например:
- Открыть файл
- Записать текст в файл
- Закрыть файл
Пример специально взят тепличный (понятно, что никакой параллелизм тут не нужен, все можно просто выполнить последовательно), но в качестве учебной задачи он вполне сойдет, а главное, на его примере отлично видна потребность в последовательном выполнении. Или вот другой пример, немного отличающийся:
- Сгенерировать три последовательности случайных чисел
- Последовательно вывести их на экран
Здесь первый пункт можно выполнять одновременно тремя разными потоками, а вот последний, вывод, нужно делать последовательно, причем только после отработки первого пункта.
В общем, задачи на параллелизм могут быть самые разные и для синхронизации потоков нужен какой-то инструмент.
2. Инструменты для синхронизации потоков
В windows.h реализовано достаточно много штатных инструментов синхронизации (так называемые «объекты синхронизации»). Среди основных: критическая область, событие, мьютекс, семафор. Да-да, для семафора уже есть реализация в windows.h. «Так зачем же его программировать?» — спросите Вы. Ну, во-первых, чтобы лучше прочувствовать, как он устроен. И, во-вторых, лишняя практика C++ никому еще не помешала 🙂
Учитывая, что мы будем использовать События, поясним, что это и как это использовать.
События используют, чтобы уведомлять ожидающие потоки. Т.е., фактически, это некоторый сигнал для потока — можно сработать или пока еще нет. Из самого смысла этого объекта вытекает, что он обладает некоторым сигнальным состоянием и возможностью его регулировки (сброс/«включение»).
Итак, после подключения windows.h мы можем создать событие с помощью:
HANDLE CreateEvent ( LPSECURITY_ATTRIBUTES lpEventAttributes, // атрибуты защиты BOOL bManualReset, // тип сброса: TRUE - ручной BOOL bInitialState, // начальное состояние: TRUE - сигнальное LPCTSTR lpName // имя объекта );
Если функция завершилась успешно, то вернется дескриптор события. Если объект не удалось создать, вернется NULL.
Чтобы поменять состояние события на сигнальное, воспользуемся функцией:
BOOL SetEvent ( HANDLE hEvent // дескриптор события );
В случае успеха вернет ненулевое значение.
Теперь про семафор. Семафор призван регулировать количество одновременно запущенных потоков. Допустим, у нас 1000 потоков, но одновременно могут работать только 2. Вот такого типа регулировка и происходит с помощью семафора. А какие функции реализованы для работы с этим объектом синхронизации?
Для создания семафора, по аналогии с event-ами:
HANDLE CreateSemaphore ( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // атрибуты доступа LONG lInitialCount, // инициализированное начальное состояние счетчика LONG lMaximumCount, // максимальное количество обращений LPCTSTR lpName // имя объекта );
При успешном выполнении получим указатель на семафор, при неудаче — NULL.
Счетчик семафора у нас постоянно меняется (поток выполняется и появляется вакантное место), поэтому состояние семафора нужно периодически менять. Делается это с помощью этой функции:
BOOL ReleaseSemaphore ( HANDLE hSemaphore, // указатель на семафор LONG lReleaseCount, // на сколько изменять счетчик LPLONG lpPreviousCount // предыдущее значение );
В случае успеха возвращаемое значение — не ноль.
Также стоит обратить внимание на функцию:
DWORD WaitForSingleObject( HANDLE hHandle, // указатель на объект, отклик от которого ожидаем DWORD dwMilliseconds // время ожидания в миллисекундах );
Из возвращаемых значений нас особенно интересуют 2: WAIT_OBJECT_0 — значит, что состояние нашего объекта сигнальное; WAIT_TIMEOUT — сигнального состояния от объекта за отведенное время мы не дождались.
3. Непосредственно задача
Итого, наше задание заключается в том, чтобы написать свои аналоги на штатные функции. Не будем сильно усложнять задачу, сделаем «реализацию в первом приближении». Главное, сохранить количественные характеристики стандартного семафора. Код с комментариями можно найти на GitHub.
В силу простоты самой задачи, особо усложнять статью не будем, но кому-то может пригодиться 🙂
ссылка на оригинал статьи https://habr.com/ru/post/476940/
Добавить комментарий