Параллельное программирование на С++ в действии. Практика разработки многопоточных программ | страница 52



Но когда для защиты одной операции приходится использовать два или более мьютексов, всплывает очередная проблема: взаимоблокировка. По природе своей она почти противоположна гонке: если в случае гонки два потока состязаются, кто придет первым, то теперь каждый поток ждет другого, и в результате ни тот, ни другой не могут продвинуться ни на шаг.

3.2.4. Взаимоблокировка: проблема и решение

Представьте игрушку, состоящую из двух частей, причем для игры необходимы обе части, — например, игрушечный барабан и палочки. Теперь вообразите двух ребятишек, которые любят побарабанить. Если одному дать барабан с палочками, то он будет радостно барабанить, пока не надоест. Если другой тоже хочет поиграть, то ему придётся подождать, как бы это ни было печально. А теперь представьте, что барабан и палочки закопаны где-то в ящике для игрушек (порознь), и оба малыша захотели поиграть с ними одновременно. Один отыскал барабан, а другой палочки. И оба оказались в тупике — если кто-то один не решится уступить и позволить поиграть другому, то каждый будет держаться за то, что имеет, требуя, чтобы другой отдал недостающее. В результате побарабанить не сможет никто.

А теперь от детей и игрушек перейдём к потокам, ссорящимся по поводу захвата мьютексов, — оба потока для выполнения некоторой операции должны захватить два мьютекса, но сложилось так, что каждый поток захватил только один мьютекс и ждет другого. Ни один поток не может продолжить, так как каждый ждет, пока другой освободит нужный ему мьютекс. Такая ситуация называется взаимоблокировкой; это самая трудная из проблем, возникающих, когда для выполнения операции требуется захватить более одного мьютекса.

Общая рекомендация, как избежать взаимоблокировок, заключается в том, чтобы всегда захватывать мьютексы в одном и том же порядке, — если мьютекс А всегда захватывается раньше мьютекса В, то взаимоблокировка не возникнет. Иногда это просто, потому что мьютексы служат разным целям, а иногда совсем не просто, например, если каждый мьютекс защищает отдельный объект одного и того же класса. Рассмотрим, к примеру, операцию сравнения двух объектов одного класса. Чтобы сравнению не мешала одновременная модификация, необходимо захватить мьютексы для обоих объектов. Однако, если выбрать какой-то определенный порядок (например, сначала захватывается мьютекс для объекта, переданного в первом параметре, а потом — для объекта, переданного во втором параметре), то легко можно получить результат, обратный желаемому: стоит двум потокам вызвать функцию сравнения, передав ей одни и те же объекты в разном порядке, как мы получим взаимоблокировку!