Наверняка, многие слышали, а кто-то встречал на практике, такие слова, как взаимные блокировки(deadlock) и гонки(race condition). Эти понятия относятся к разряду ошибок в использовании concurrency. Если я задам вам вопрос, что такое дедлок, вы с большой вероятностью без доли сомнения начнете рисовать классическую картинку дедлока или его представление в псевдокоде. Что-то вроде этого:
Эту информацию мы получаем в институте, можно найти в книжках и статьях на просторах интернета. Такой дедлок с использованием, например, двух мьютексов, во всей своей красе можно встретить в коде. Но в большинстве случаев не все так просто, и не каждый может увидеть классический паттерн ошибки в коде, если он представлен не в привычном виде.
Рассмотрим класс, в котором нас интересуют методы StartUpdate, CheckAndUpdate и Stop, используется C++, код максимально упрощен:
На что следует обратить внимание в представленном коде:
Теперь проанализируем всю полученную информацию и составим картинку:
Принимая во внимание два представленных выше факта, нетрудно сделать вывод, что попытка захвата рекурсивного мьютекса в одной из функций приведет к ожиданию освобождения мьютекса, если он уже был захвачен в другой функции, поскольку колбэк CheckAndUpdate всегда выполняется в отдельном потоке.
На первый взгляд ничего подозрительного, относящегося к дедлоку нет. Но если быть повнимательнее, то все сводится к нашей классической картинке. Когда начинает выполняться функциональный объект, мы как бы неявно захватываем ресурс m_future, колбэк напрямую
ассоциируется с m_future:
Порядок действий, приводящих к дедлоку, таков:
Вот и все: поток, выполняющий вызов Stop, ждет завершения выполнения CheckAndUpdate, а другой поток в свою очередь не может продолжить работу, пока не захватит мьютекс, который уже захвачен упомянутым ранее потоком. Вполне себе классический дедлок. Пол дела сделано – обнаружена причина проблемы.
Вот такой незамысловатый пример с неочевидным дедлоком легко сводится к паттерну этой ошибки. Напоследок хочу пожелать вам писать надежный и потокобезопасный код!
Эту информацию мы получаем в институте, можно найти в книжках и статьях на просторах интернета. Такой дедлок с использованием, например, двух мьютексов, во всей своей красе можно встретить в коде. Но в большинстве случаев не все так просто, и не каждый может увидеть классический паттерн ошибки в коде, если он представлен не в привычном виде.
Рассмотрим класс, в котором нас интересуют методы StartUpdate, CheckAndUpdate и Stop, используется C++, код максимально упрощен:
std::recursive_mutex m_mutex;
Future m_future;
void Stop()
{
std::unique_lock scoped_lock(m_mutex);
m_future.Wait();
// do something
}
void StartUpdate()
{
m_future.Wait();
m_future = Future::Schedule(std::bind(&Element::CheckAndUpdate, this),
std::chrono::milliseconds(100);
}
void CheckAndUpdate()
{
std::unique_lock scoped_lock(m_mutex);
//do something
}
На что следует обратить внимание в представленном коде:
- используется рекурсивный мьютекс. Неоднократный захват рекурсивного мьютекса не приводит к ожиданию только, если эти захват происходит в том же потоке. При этом количество освобождений мьютекса должно соответствовать количеству захватов. Если же мы пытаемся захватить рекурсивный мьютекс, который уже захвачен в другом потоке, поток переходит в режим ожидания.
- функция Future::Schedule запускает (через n миллисекунд) в отдельном потоке передаваемый в нее колбэк
Теперь проанализируем всю полученную информацию и составим картинку:
Принимая во внимание два представленных выше факта, нетрудно сделать вывод, что попытка захвата рекурсивного мьютекса в одной из функций приведет к ожиданию освобождения мьютекса, если он уже был захвачен в другой функции, поскольку колбэк CheckAndUpdate всегда выполняется в отдельном потоке.
На первый взгляд ничего подозрительного, относящегося к дедлоку нет. Но если быть повнимательнее, то все сводится к нашей классической картинке. Когда начинает выполняться функциональный объект, мы как бы неявно захватываем ресурс m_future, колбэк напрямую
ассоциируется с m_future:
Порядок действий, приводящих к дедлоку, таков:
- Планируется выполнение CheckAndUpdate, но колбэк стартует не сразу, через n миллисекунд.
- Вызывается Stop метод, и тут понеслась: пытаемся захватить мьютекс – ресурс один захвачен, начинаем ждать окончания выполнения m_future – вызова объекта пока еще не было, ждем.
- Начинается выполнение CheckAndUpdate: пытаемся захватить мьютекс – не можем, ресурс уже захвачен другим потоком, ожидаем освобождения.
Вот и все: поток, выполняющий вызов Stop, ждет завершения выполнения CheckAndUpdate, а другой поток в свою очередь не может продолжить работу, пока не захватит мьютекс, который уже захвачен упомянутым ранее потоком. Вполне себе классический дедлок. Пол дела сделано – обнаружена причина проблемы.
Теперь немного о том, как это исправить
Подход 1
Порядок захвата ресурсов должен быть одинаковым, это позволит избежать дедлоков. То есть нужно посмотреть, возможно ли поменять порядок захвата ресурсов в методе Stop. Так как здесь случай дедлока не совсем очевидный, и явный захват ресурса m_future в CheckAndUpdate отсутствует, решили подумать над другим решением во избежание возвращения ошибки в будущем.
Подход 2
Порядок захвата ресурсов должен быть одинаковым, это позволит избежать дедлоков. То есть нужно посмотреть, возможно ли поменять порядок захвата ресурсов в методе Stop. Так как здесь случай дедлока не совсем очевидный, и явный захват ресурса m_future в CheckAndUpdate отсутствует, решили подумать над другим решением во избежание возвращения ошибки в будущем.
Подход 2
- Проверить, можно ли отказаться от использования мьютекса в CheckAndUpdate.
- Раз у нас используется механизм синхронизации, то мы ограничиваем доступ к каким-то ресурсам. Возможно, вам будет достаточно переделать эти ресурсы в атомики(как это было у нас), доступ к которым уже потокобезопасный.
- Оказалось, что переменные, доступ к которым ограничивался, можно легко переделать в атомики, поэтому упомянутый мьютекс успешно удаляется.
Вот такой незамысловатый пример с неочевидным дедлоком легко сводится к паттерну этой ошибки. Напоследок хочу пожелать вам писать надежный и потокобезопасный код!
Комментарии (2)
qw1
03.03.2019 23:03Теперь немного о том, как это исправить
Да ну нафиг.
Как минимум, непонятны ожидания от StartUpdate — иногда он блокирует поток, иногда нет. В идеале, нужно сделать так, что если CheckAndUpdate ещё не стартовал, то StartUpdate ничего не делает.
Но вообще непонятно, что защищает m_mutex. В Future уже есть синхронизация, в этом рафинированном примере m_mutex не нужен.
anger32
wound/wait mutex?