Страницы

Поиск по вопросам

суббота, 30 ноября 2019 г.

Как избавиться от deadlock при использовании mutex?

#c++ #многопоточность


В первом потоке так:

mutex1.lock();
mutex2.lock();


Во втором так:

mutex2.lock();
mutex1.lock();


Проблема в том, что эти мьютексы находятся в разных классах, скажем Parent и Child,
они вызывают методы друг друга, но ничего не знают о внутреннем устройстве друг друга
(но если надо, их можно научить, но только как?). Мьютексы защищают какие-то внутренние
данные соответствующих классов.

Это происходит при вложенных вызовах:

Callstack - thread 1

10. mutex2.lock();
 9. child.method2();
 8. mutex1.lock();
 7. parent.method1();
....


Callstack - thread 2

10. mutex1.lock(); 
 9. parent.anotherMethod();
 8. mutex2.lock();
 7. child.someMethod();
....


Может есть какой-то паттерн для такой проблемы.
    


Ответы

Ответ 1



Чтобы избавиться от дедлоков, можно использовать функцию std::lock. Она принимает несколько мьютексов, и лочит их с помощью специального алгоритма который позволяет избежать дедлоков. Применять ее следует таким образом: std::lock(mutex1, mutex2); std::lock_guard guard1(mutex1, std::adopt_lock); std::lock_guard guard2(mutex2, std::adopt_lock); Или наоборот, сначала создать lock_guard, а потом залочить: std::lock_guard guard1(mutex1, std::defer_lock); std::lock_guard guard2(mutex2, std::defer_lock); std::lock(mutex1, mutex2);

Ответ 2



Обновление: В вашем случае, когда у каждого класса свой мьютекс, и проблема лишь в порядке вызовов, я бы очень не рекомендовал вызывать чужой код под блокировкой. Именно потому, что этот код имеет полное право залочить какой-то другой мьютекс. Вызов чужого кода под залоченным мьютексом — практически всегда опасность deadlock'а. Попробуйте реорганизовать код так, чтобы он выглядел следующим образом: void obj1.method1() { { std::lock_guard g1(mutex1); // работаем с данными/запоминаем их } // только здесь мы имеем право вызвать объект, который мы не контролируем obj2.method2(); } Вы не можете выбраться из этой ситуации. Классы, использующие общие мьютексы, должны кооперироваться. Часто проблема решается таким образом: мьютексы перенумеровываются, и если код хочет взять несколько мьютексов, он обязан брать их в порядке возрастания номеров. С другой стороны, можно уменьшить гранулярность блокировки, и использовать один мьютекс вместо обоих (если это допустимо в рамках вашей задачи). Также, вы можете попытаться избежать проблемы двойного мьютекса тем, что разделите работу с блокируемыми объектами на части/сущности: сначала залочите один мьютекс и извлечёте нужные данные, затем отпустите этот мьютекс и залочите другой, используя при работе со вторым объектом локально сохранённые данные. Ещё один подход, завоёвывающий популярность — избавиться по возможности от разделяемых данных, и работать как можно больше с локальными данными. Резюме: общего решения не существует. Выкручивайтесь.

Ответ 3



Вам уже ответили очень много полезного, хотелось бы добавить еще пару моментов : в случае, когда работа с данными постороена таким образом, что никак не избежать захватов двух мьютексов, советуют предопределить очередность захвата этих мьютексов, и во всех потоках придерживаться этой очередности; в тех случаях, когда архитектура не позволяет предопределить очередность захвата мьютексов, можно использовать std::try_lock(), которая в случае невозможности захвата мьютекса, возвращает управление. Таким образом, если поток захватил первый мьютекс, но не может захватить второй, - он может освободить первый, выполнить какую-то другую рутинную работу (вместо того, чтобы висеть на одном из мьютексов), и вернуться к попытке захвата этих мьютексов вновь.

Комментариев нет:

Отправить комментарий