Сегодня коротко расскажу о том, как я реализовывал семафор на основании объекта синхронизации «Событие».
Сначала пройдусь по определениям.
Очевидно, что набор действий мы можем выполнять несколькими способами. Самые простые — последовательно и параллельно. Параллельности выполнения определенных действий можно достигнуть за счет запуска различных потоков (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.
В силу простоты самой задачи, особо усложнять статью не будем, но кому-то может пригодиться :)
Комментарии (8)
loginsin
22.11.2019 00:073. Непосредственно задача
Итого, наше задание заключается в том, чтобы написать свои аналоги на штатные функции...
Э… и все? Я б тему бы с семафоров расширил бы до вообще организации мультипоточности средствами WinAPI (а не windows.h...). Еще бы про lock-free всякое порассказывал. У WinAPI в этом плане о-очень богатый инструментарий.
Sazonov
22.11.2019 01:51А разве с включённой оптимизацией ваш последний цикл не должен развернуться в while(true)? Вы бы хотяб atomic int взяли.
Если честно, не понял посыла статьи. Вы написали что-то похожее на C++ обвёртку поверх WinAPI? Вполне можно было реализовать это на чистом C++ без платформозависимого кода. Не говоря уже о том, что в WinAPI есть готовые семафоры.
mayorovp
22.11.2019 07:10Как можно писать вот так?
while (alive_threads != 0) {}
Хоть бы
SwitchToThread()
поставил… А лучше — WaitForMultipleObjects (напомню, что хендл потока тоже можно передавать туда!)
oleg-m1973
22.11.2019 15:43+1А как у вас синхронизируются WaitForSemaphore и LeaveSemaphore, что будет, если их вызвать одновременно из нескольких потоков?
По-моему никак и лучше их не вызывать одновременно, так?
jcmvbkbc
23.11.2019 11:03+2В силу простоты самой задачи, особо усложнять статью не будем, но кому-то может пригодиться :)
А зря, потому что реализация не выдерживает никакой критики, и, очевидно, не тестировалась.
Fyret
Есть какой-то скрытый смысл в том, что объект уничтожается вот так
вместо банального
?
Оставим пока современнный C++ с его умными указателями за скобками.
Altren
> Есть какой-то скрытый смысл в том, что объект уничтожается вот так
Вероятно чтобы получить утечку.
На всякий случай ссылка для сомневающихся: stackoverflow.com/questions/7155330/is-memory-released-when-a-destructor-is-called-or-when-delete-is-called
monah_tuk
Код не изучал, но там нет placement new?да, там plamenet new нет.