Сегодня коротко расскажу о том, как я реализовывал семафор на основании объекта синхронизации «Событие».

Сначала пройдусь по определениям.

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)


  1. Fyret
    21.11.2019 23:49
    +2

    Есть какой-то скрытый смысл в том, что объект уничтожается вот так

    s -> ~Semaphore(); // destroying semaphore

    вместо банального
    delete s;

    ?
    Оставим пока современнный C++ с его умными указателями за скобками.


    1. Altren
      22.11.2019 01:52

      > Есть какой-то скрытый смысл в том, что объект уничтожается вот так
      Вероятно чтобы получить утечку.
      На всякий случай ссылка для сомневающихся: stackoverflow.com/questions/7155330/is-memory-released-when-a-destructor-is-called-or-when-delete-is-called


    1. monah_tuk
      25.11.2019 05:00

      Код не изучал, но там нет placement new? да, там plamenet new нет.


  1. loginsin
    22.11.2019 00:07

    3. Непосредственно задача

    Итого, наше задание заключается в том, чтобы написать свои аналоги на штатные функции...


    Э… и все? Я б тему бы с семафоров расширил бы до вообще организации мультипоточности средствами WinAPI (а не windows.h...). Еще бы про lock-free всякое порассказывал. У WinAPI в этом плане о-очень богатый инструментарий.


  1. Sazonov
    22.11.2019 01:51

    А разве с включённой оптимизацией ваш последний цикл не должен развернуться в while(true)? Вы бы хотяб atomic int взяли.
    Если честно, не понял посыла статьи. Вы написали что-то похожее на C++ обвёртку поверх WinAPI? Вполне можно было реализовать это на чистом C++ без платформозависимого кода. Не говоря уже о том, что в WinAPI есть готовые семафоры.


  1. mayorovp
    22.11.2019 07:10

    Как можно писать вот так?


        while (alive_threads != 0) {}

    Хоть бы SwitchToThread() поставил… А лучше — WaitForMultipleObjects (напомню, что хендл потока тоже можно передавать туда!)


  1. oleg-m1973
    22.11.2019 15:43
    +1

    А как у вас синхронизируются WaitForSemaphore и LeaveSemaphore, что будет, если их вызвать одновременно из нескольких потоков?
    По-моему никак и лучше их не вызывать одновременно, так?


  1. jcmvbkbc
    23.11.2019 11:03
    +2

    В силу простоты самой задачи, особо усложнять статью не будем, но кому-то может пригодиться :)

    А зря, потому что реализация не выдерживает никакой критики, и, очевидно, не тестировалась.