Ограниченность семафоров
В Windows существуют важные ограничения, касающиеся реализации семафоров. Например, каким образом поток может потребовать, чтобы счетчик семафора уменьшился на 2? Для этого поток мог бы организовать ожидание два раза подряд, как показано ниже, но эта операция не была бы атомарной, поскольку в промежутке между двумя вызовами функции ожидания данный поток может быть вытеснен. В результате этого, как описывается ниже, может наступить взаимоблокировка (deadlock) потоков.
/* hsem – дескриптор семафора. Максимальное значение счетчика семафора равно 2. */
…
/* Уменьшить значение счетчика семафора на 2. */
WaitForSingleObject(hSem, INFINITE);
WaitForSingleObject(hSem, INFINITE);
…
/* Увеличить значение счетчика семафора на 2. */
ReleaseSemaphore(hSem, 2, &PrevCount);
Чтобы увидеть, каким образом в подобной ситуации может возникнуть взаимоблокировка, предположим, что максимальное и начальное значения счетчика устанавливаются равными 2 и что первый из двух потоков завершает первый цикл ожидания, а затем вытесняется. Далее второй поток может завершить первый цикл ожидания и уменьшить значение счетчика до 0. Оба потока окажутся блокированными на неопределенное время, поскольку ни одна из них не сможет выполнить второй цикл ожидания. Такая простая ситуация взаимоблокировки является довольно типичной.
Один из возможных вариантов правильного решения заключается в том, чтобы защитить циклы ожидания при помощи мьютекса или объекта CRITI-CAL_SECTION, как показано в приведенном ниже фрагменте программного кода:
/* Уменьшаем значение счетчика семафора на 2. */
EnterCriticalSection(&csSem);
WaitForSingleObject(hSem, INFINITE);
WaitForSingleObject(hSem, INFINITE);
LeaveCriticalSection (&csSem);
…
ReleaseSemaphore(hSem, 2, &PrevCount);
Но и эта реализация, в таком общем виде, страдает ограничениями. Предположим, например, что в счетчике семафора остается две единицы, и потоку А необходимы три единицы, а потоку В — только две. Если первой начнет выполняться поток А, то он выполнит два цикла ожидания и блокируется на третьем, продолжая владеть мьютексом. При этом поток В, которому были необходимы только две единицы, по-прежнему будет оставаться блокированным.
Казалось бы, можно воспользоваться функцией WaitForMultipleObjects с использованием одного и того же дескриптора семафора в нескольких элементах массива дескрипторов. Однако такое предложение было бы неудачным по двум причинам. Прежде всего, обнаружив, что два дескриптора указывают на один и тот же объект, функция WaitForMultipleObjects завершится с ошибкой. Более того, даже если значение счетчика семафора будет составлять только 1, сигнализироваться будут все дескрипторы, что противоречит самой исходной цели.
Полное решение проблемы множественных циклов ожидания предлагается в упражнении 10.11.
Проектировать семафоры Windows было бы гораздо удобнее, если бы существовала возможность выполнять множественные циклы ожидания в виде одной атомарной операции (atomic multiple-wait operation).