Дополнительные рекомендации относительно использования мьютексов и объектов CRITICAL_SECTION
Дополнительные рекомендации относительно использования мьютексов и объектов CRITICAL_SECTION
К этому времени мы успели познакомиться со всеми объектами синхронизации Windows и исследовали их применимость на ряде примеров. Мьютексы и объекты CS рассматривались первыми, а поскольку события мы еще будем интенсивно использовать в следующей главе, то настоящую главу целесообразно завершить рекомендациями относительно применения мьютексов и объектов CS для обеспечения корректности выполнения, удобства сопровождения и повышения производительности программ.
Приведенные ниже утверждения сформулированы, как правило, в терминах мьютексов, однако, если не оговорено иное, все сказанное относится и к объектам CS.
• Если функция WaitForSingleObject, одним из аргументов которой является дескриптор мьютекса, вызывается без использования конечного интервала ожидания, то вызывающий поток может оказаться блокированным на неопределенное время. Ответственность за то, чтобы захваченный (блокированный) мьютекс в конечном счете был освобожден (разблокирован), возлагается на программиста.
• Если поток завершает выполнение или его выполнение прерывается до того, как он покинет (разблокирует) объект CS, то этот объект остается блокированным. Чрезвычайно полезным свойством мьютексов является то, что владеющий ими поток может завершить выполнение, не уступив прав владения мьютексом.
• Не пытайтесь получить доступ к ресурсам, защищаемым мьютексом, если функция WaitForSingleObject вызвана с использованием конечного интервала ожидания.
• Ожидать перехода блокированного мьютекса в сигнальное состояние могут сразу несколько потоков. Когда мьютекс освобождается, то только один из ожидающих потоков получает права владения мьютексом и переводится в состояние готовности планировщиком ОС на основании действующей стратегии приоритетов и планирования. Не следует делать никаких предположений относительно того, что какой-либо поток будет пользоваться приоритетом; как и в любом другом случае, программу следует проектировать таким образом, чтобы приложение работало корректно независимо от того, какой именно из ожидающих потоков получит права владения мьютексом и возобновит выполнение. Те же замечания остаются справедливыми и в отношении потоков, ожидающих наступления события; никогда не следует предполагать, что при переходе объекта события в сигнальное состояние освободится какой-то определенный поток или что потоки будут разблокированы в какой-то определенной очередности.
• К критическому участку кода относятся все операторы, расположенные между точками программы, в которых поток приобретает права владения мьютексом и уступает их. Для определения нескольких критических участков кода может быть использован один и тот же мьютекс. Корректная организация программы предполагает, что критический участок кода, определяемый мьютексом, в каждый момент времени может выполняться только одним потоком.
• Определяемая мьютексами степень детализации программы, или гранулярность мьютексов (mutex granularity), оказывает влияние на производительность и требует серьезного рассмотрения. Размер каждого критического участка кода ни в коем случае не должен превышать необходимой величины, и мьютекс не должен захватываться на более длительный промежуток времени, чем это необходимо. Использование критических участков кода чрезмерно большого размера, захватываемых на длительные промежутки времени, снижает параллелизм и может оказывать отрицательное влияние на производительность.
• Связывайте мьютекс непосредственно с ресурсом, защиту которого он должен обеспечивать, возможно, с использованием структуры данных. (Именно эта методика задействована в программах 8.1 и 8.2.)
• Максимально точно документируйте инвариант, используя для этого словесные описания либо логические, или булевские, выражения. Инвариант— это свойство защищаемого ресурса, сохранение которого неизменным вне критического участка кода вы гарантируете. Форма выражения инвариантов может быть самой различной: "элемент принадлежит обоим спискам или не принадлежит ни одному из них", "контрольная сумма данных в буфере является достоверной", "связанный список является действительным" или "0 <= nLost + nCons <= sequence". Точно сформулированные инварианты могут использоваться совместно с макросом ASSERT при отладке программ, хотя оператор ASSERT должен иметь собственный критический участок кода.
• Убедитесь в том, что каждый критический участок кода имеет только одну точку входа, в которой поток блокирует мьютекс, и только одну точку выхода, в которой поток освобождает мьютекс. Избегайте использования сложных операторов ветвления и таких операторов, как break, return или goto, предоставляющих возможность выхода за пределы критического участка кода. Для защиты от подобных рисков оказываются удобными обработчики завершения.
• Если требуемая логика работы программы приводит к чрезмерному разрастанию критического участка кода (скажем, его размер превышает одну страницу), попробуйте разместить этот код в отдельной функции, чтобы можно было легко понять схему синхронизации. Так, целесообразно выделить в отдельную функцию код, предназначенный для удаления узла из сбалансированного дерева поиска, пока дерево остается блокированным.