Читаем без скачивания Системное программирование в среде Windows - Джонсон Харт
Шрифт:
Интервал:
Закладка:
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpsa, BOOL bInitialOwner, LPCTSTR lpMutexName)
BOOL ReleaseMutex(HANDLE hMutex)
bInitialOwner — если значение этого флага установлено равным True, вызывающий поток немедленно приобретает права владения новым мьютексом. Эта атомарная операция позволяет предотвратить приобретение прав владения мьютексом другими потоками, прежде чем это сделает поток, создающий мьютекс. Как следует из самого его названия (initial owner — исходный владелец), этот флаг не оказывает никакого действия, если мьютекс уже существует.
lpMutexName — указатель на строку, содержащую имя мьютекса; в отличие от файлов имена мьютексов чувствительны к регистру. Если этот параметр равен NULL, то мьютекс создается без имени. События, мьютексы, семафоры, отображения файлов и другие объекты ядра, упоминаемые в данной книге, — все они используют одно и то же пространство имен, отличное от пространства имен файловой системы. Поэтому имена всех объектов синхронизации должны быть различными. Длина указанных имен не может превышать 260 символов.
Возвращаемое значение имеет тип HANDLE; значение NULL указывает на неудачное завершение функции.
Функция OpenMutex открывает существующий именованный мьютекс. Впоследствии эта функция не обсуждается, но используется в некоторых примерах. Эта функция дает возможность потокам, принадлежащим различным процессам, синхронизироваться так, как если бы они принадлежали одному и тому же процессу. Вызову функции OpenMutex в одном процессе должен предшествовать вызов функции CreateMutex в другом процессе. Для семафоров и событий, как и для отображенных файлов (глава 5), также имеются соответствующие функции Create и Open. При вызове этих функций всегда предполагается, что сначала один процесс, например сервер, вызывает функцию Create для создания именованного объекта, а затем другие процессы вызывают функцию Open, которая завершается неудачей, если именованный объект к этому моменту еще не был создан. Возможен и такой вариант, когда все процессы самостоятельно используют вызов функции Create с одним и тем же именем, если порядок создания объектов не имеет значения.
Функция ReleaseMutex освобождает мьютекс, которым владеет вызывающий поток. Если мьютекс не принадлежит потоку, функция завершается с ошибкой.
BOOL ReleaseMutex(HANDLE hMutex)
Спецификация POSIX Pthreads поддерживает мьютексы. Имеются следующие основные функции:
• pthread_mutex_init
• pthread_mutex_destroy
• pthread_mutex_lock
• pthread_mutex_unlock
Функция pthread_mutex_lock является блокирующей и поэтому эквивалентна функции WaitForSingleObject в случае ее применения к дескриптору мьютекса. Функция pthread_mutex_trylock осуществляет опрос и не является блокирующей, соответствуя функции WaitForSingleObject в случае ее применения с нулевым значением интервала ожидания. Потоки Pthreads не поддерживают конечные интервалы ожидания и не предлагают средств, аналогичных Windows-объектам CRITICAL_SECTION.
Покинутые мьютексы
Мьютекс, владевший которым поток завершился, не освободив его, называют покинутым (abandoned), и его дескриптор переходит в сигнальное состояние. На то, что сигнализирующий дескриптор (дескрипторы) представляет покинутый мьютекс (мьютексы), указывает возврат функцией WaitForSingleObject значения WAIT_ABANDONED_0 или использование значения WAIT_ABANDONED_0 в качестве базового значения функцией WaitForMultipleObject.
То, что дескрипторы покинутых мьютексов переходят в сигнальное состояние, является весьма полезным их свойством, недоступным в случае объектов CS. Обнаружение покинутого мьютекса может означать наличие дефекта в коде, организующем работу потоков, поскольку потоки должны программироваться таким образом, чтобы ресурсы всегда освобождались, прежде чем поток завершит свое выполнение. Возможно также, что выполнение данного потока было прервано другим потоком.
Мьютексы, критические участки кода и взаимоблокировки
Несмотря на то что объекты CS и мьютексы обеспечивают решение задач, подобных той, которая иллюстрируется на рис. 8.1, при их использовании следует соблюдать осторожность, иначе можно создать ситуацию взаимоблокировки (deadlock), в которой каждый из двух потоков ждет освобождения ресурсов, принадлежащих другому потоку.
Взаимоблокировки являются одним из наиболее распространенных и коварных дефектов синхронизации и часто возникают, когда должны быть одновременно блокированы (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, который разблокировал бы другой поток.
Обратите внимание, что взаимоблокировка по сути дела является еще одной разновидностью состязаний, поскольку каждый из потоков состязается с другим за право первым овладеть всеми своими мьютексами.
Один из способов, позволяющих избежать взаимоблокировки, заключается в применении метода "проб и ошибок", когда поток вызывает функцию WaitForSingleObject с конечным интервалом ожидания, и если оказывается, что мьютекс уже принадлежит другому потоку, то первый поток уступает процессор или "засыпает" на короткое время, а затем вновь повторяет попытку. Намного лучше и эффективнее с самого начала проектировать программу таким образом, чтобы исключить саму возможность возникновения взаимоблокировок, о чем говорится ниже.
Гораздо более простой метод, который описывается почти в любом учебнике по ОС, заключается в предварительном определении "иерархии мьютексов" и программировании потоков таким образом, чтобы захват ими мьютексов осуществлялся в строгом соответствии с заданным иерархическим порядком, а освобождение — в обратном порядке. Эта иерархия может устанавливаться произвольно или естественным образом определяться структурой самой задачи, но в любом случае ее должны придерживаться все потоки. В данном примере лишь требуется, чтобы функция удаления мьютекса поочередно ожидала освобождения списков А и В, и тогда взаимоблокировка потоков никогда не случится, если указанная иерархическая очередность будет соблюдаться всеми потоками в любом месте программы.
Еще одним действенным методом снижения риска взаимоблокировки является размещение двух дескрипторов мьютексов в массиве и использование функции WaitForMultipleObjects с флагом fWaitAll, значение которого установлено равным True, вследствие чего поток в результате выполнения одной атомарной операции будет захватывать либо оба мьютекса, либо ни одного. В случае использования объектов CRITICAL_SECTION описанная методика неприменима.