Сегодня коротко расскажу о том, как я реализовывал семафор на основании объекта синхронизации «Событие».
Сначала пройдусь по определениям.
Очевидно, что набор действий мы можем выполнять несколькими способами. Самые простые — последовательно и параллельно. Параллельности выполнения определенных действий можно достигнуть за счет запуска различных потоков (threads). Идея простая: назначаем каждому потоку какое-то элементарное (или не очень) действие и запускаем их в определенном порядке. Вообще говоря, запустить мы их можем и все одновременно — выигрыш по времени мы, конечно, получим. Это понятно: одно дело вывести 10 000 слов одно за другим, а другое дело одновременно выводить, например, 100 слов. 100-кратный выигрыш по времени (плюс-минус, без учета задержек и проч.). Но исходная задача может предполагать строгую последовательность действий.
Например:
Пример специально взят тепличный (понятно, что никакой параллелизм тут не нужен, все можно просто выполнить последовательно), но в качестве учебной задачи он вполне сойдет, а главное, на его примере отлично видна потребность в последовательном выполнении. Или вот другой пример, немного отличающийся:
Здесь первый пункт можно выполнять одновременно тремя разными потоками, а вот последний, вывод, нужно делать последовательно, причем только после отработки первого пункта.
В общем, задачи на параллелизм могут быть самые разные и для синхронизации потоков нужен какой-то инструмент.
В windows.h реализовано достаточно много штатных инструментов синхронизации (так называемые «объекты синхронизации»). Среди основных: критическая область, событие, мьютекс, семафор. Да-да, для семафора уже есть реализация в windows.h. «Так зачем же его программировать?» — спросите Вы. Ну, во-первых, чтобы лучше прочувствовать, как он устроен. И, во-вторых, лишняя практика C++ никому еще не помешала :)
Учитывая, что мы будем использовать События, поясним, что это и как это использовать.
События используют, чтобы уведомлять ожидающие потоки. Т.е., фактически, это некоторый сигнал для потока — можно сработать или пока еще нет. Из самого смысла этого объекта вытекает, что он обладает некоторым сигнальным состоянием и возможностью его регулировки (сброс/«включение»).
Итак, после подключения windows.h мы можем создать событие с помощью:
Если функция завершилась успешно, то вернется дескриптор события. Если объект не удалось создать, вернется NULL.
Чтобы поменять состояние события на сигнальное, воспользуемся функцией:
В случае успеха вернет ненулевое значение.
Теперь про семафор. Семафор призван регулировать количество одновременно запущенных потоков. Допустим, у нас 1000 потоков, но одновременно могут работать только 2. Вот такого типа регулировка и происходит с помощью семафора. А какие функции реализованы для работы с этим объектом синхронизации?
Для создания семафора, по аналогии с event-ами:
При успешном выполнении получим указатель на семафор, при неудаче — NULL.
Счетчик семафора у нас постоянно меняется (поток выполняется и появляется вакантное место), поэтому состояние семафора нужно периодически менять. Делается это с помощью этой функции:
В случае успеха возвращаемое значение — не ноль.
Также стоит обратить внимание на функцию:
Из возвращаемых значений нас особенно интересуют 2: WAIT_OBJECT_0 — значит, что состояние нашего объекта сигнальное; WAIT_TIMEOUT — сигнального состояния от объекта за отведенное время мы не дождались.
Итого, наше задание заключается в том, чтобы написать свои аналоги на штатные функции. Не будем сильно усложнять задачу, сделаем «реализацию в первом приближении». Главное, сохранить количественные характеристики стандартного семафора. Код с комментариями можно найти на GitHub.
В силу простоты самой задачи, особо усложнять статью не будем, но кому-то может пригодиться :)
Сначала пройдусь по определениям.
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.
В силу простоты самой задачи, особо усложнять статью не будем, но кому-то может пригодиться :)