То, что дескрипторы покинутых мьютексов переходят в сигнальное состояние, является весьма полезным их свойством, недоступным в случае объектов CS. Обнаружение покинутого мьютекса может означать наличие дефекта в коде, организующем работу потоков, поскольку потоки должны программироваться таким образом, чтобы ресурсы всегда освобождались, прежде чем поток завершит свое выполнение. Возможно также, что выполнение данного потока было прервано другим потоком.
Мьютексы, критические участки кода и взаимоблокировки
Несмотря на то что объекты CS и мьютексы обеспечивают решение задач, подобных той, которая иллюстрируется на рис. 8.1, при их использовании следует соблюдать осторожность, иначе можно создать ситуацию
Взаимоблокировки являются одним из наиболее распространенных и коварных дефектов синхронизации и часто возникают, когда должны быть одновременно блокированы (lock) два и более мьютекса. Рассмотрим следующую задачу:
• Имеется два связных списка, список А и список В, каждый из которых содержит идентичные структуры и поддерживается рабочими потоками.
• Для одного класса элементов списка корректность операции зависит от того факта, что данный элемент X находится или отсутствует одновременно в обоих списках. Здесь мы имеем дело с инвариантом, который неформально можно выразить так: "X либо находится в обоих списках, либо не находится ни в одном из них".
• В других ситуациях допускается нахождение элемента только в одном из списков, но не в обоих одновременно.
• В связи с вышеизложенным для обоих списков требуются различные мьютексы (объекты CS), но при добавлении или удалении общих элементов списков блокироваться должны одновременно оба мьютекса. Использование только одного мьютекса оказало бы отрицательное влияние на производительность, препятствуя независимому параллельному обновлению двух списков, поскольку мьютекс оказался бы "слишком большим".
Ниже приведен пример возможной реализации функций рабочего потока, предназначенных для добавления и удаления общих элементов списков:
static struct {
/* Инвариант: действительность списка. */
HANDLE guard; /* Дескриптор мьютекса. */
struct ListStuff;
} ListA, ListB;
…
DWORD WINAPI AddSharedElement(void *arg) /* Добавляет общий элемент в списки А и В. */
{ /* Инвариант: новый элемент либо находится в обоих списках, либо не находится ни в одном из них. */
WaitForSingleObject(ListA.guard, INFINITE);
WaitForSingleObject(ListB.guard, INFINITE);
/* Добавить элемент в оба списка … */
ReleaseMutex(ListB.guard);
ReleaseMutex(ListA.guard);
return 0;
}
DWORD WINAPI DeleteSharedElement(void *arg) /* Удаляет общий элемент из списков А и В. */
{
WaitForSingleObject(ListB.guard, INFINITE);
WaitForSingleObject(ListA.guard, INFINITE);
/* Удалить элемент из обоих списков … */
ReleaseMutex(ListB.guard);
ReleaseMutex(ListA.guard);
return 0;
}
С учетом ранее данных рекомендаций этот код выглядит вполне корректным. Однако вытеснение потока AddSharedElement сразу же после того, как он блокирует список А, и непосредственно перед тем, как он попытается заблокировать список В, приведет к взаимоблокировке потоков, если поток DeleteSharedElement начнет выполняться до того, как возобновится выполнение потока AddSharedElement. Каждый из потоков владеет мьютексом, который необходим другому потоку, и ни один из потоков не может перейти к вызову функции ReleaseMutex, который разблокировал бы другой поток.
Обратите внимание, что взаимоблокировка по сути дела является еще одной разновидностью состязаний, поскольку каждый из потоков состязается с другим за право первым овладеть всеми своими мьютексами.