avva: (Default)
[personal profile] avva
(интересно будет скорее всего только программистам)

Пару недель назад я починил баг на работе в одной внутренней библиотеке. Этот баг проявлялся так: долгоживущие сервера, пользовавшиеся этой библиотекой определенным способом, загадочным и случайным образом падали примерно раз в неделю (не все одновременно, а каждый сервер отдельно падал примерно с такой частотой). Найти и починить баг оказалось очень тяжело, потому что к моменту падения следов никаких почти не оставалось. Сам по себе баг тривиален - многопоточный код недостаточно дисциплинированно охранял доступ к глобальному ресурсу, который я назову X. У X есть мьютекс, который надо получить перед тем, как его трогать. Два потока делали следующее:

thread 1: get-mutex-for-X change1-X release-mutex-for-X
thread 2: change2-X

Код, который меняет X, в двух потоках был разный, поэтому я обозначил его change1 и change2, но он трогал одну и ту же часть X, и второй поток банально не охранял свой доступ мьютексом. Изредка, очень изредка, эти два потока выполняли эти куски кода одновременно на разных ядрах. Отыскать это все быстро мешало несколько осложняющих обстоятельств:

- кроме этих двух мест, есть еще десятки мест, которые меняют X, правильно охраняя замену мьютексом, но ни один из них не может конфликтовать с "thread 2"
- сам конфликт между thread 1 и thread 2 не приводил к немедленному падению, а коррумпировал внутренние структуры X, что обычно приводило к падению с загадочной и невнятной ошибкой спустя несколько секунд. К тому времени обычно thread 1 и thread 2 давно уже ушли из этих частей кода и никак не проявлялись в stack trace. Обычно там проявлялись другие места, которые ждали mutex-for-X в момент падения, просто по случайности.
- тот факт, что thread 2 не пользовался мьютексом, не был очевиден из его кода - неважно почему, долго объяснять. Казалось бы, этот код полностью лежал, вместе с еще кучей всего, внутри мьютекса X, но на самом деле он освобождал мьютекс как раз перед change2, и потом брал его заново, и это было неочевидно при чтении кода. Т.е. thread 2 выглядел, если быть точнее, так - троеточие передает "много всякой всячины":
thread 2: get-mutex-for-X ... release-mutex-for-X change2-X get-mutex-for-X ... release-mutex-for-X

Во многих ситуациях такое временное освобождение мьютекса как раз правильное поведение - если бы change2-X не меняло X, а делало какие-то совершенное другие вещи, и притом не очень быстро, то как раз это правильно. Но в данном случае это ошибка.

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

thread 1: get-mutex-for-X change1-X release-mutex-for-X
thread 2: get-mutex-for-X ... change2-X ... release-mutex-for-X

Падений больше не было. Вскоре я уехал в отпуск.

Во время моего отпуска, который еще не закончился, выяснилось следующее. Кусок кода из "thread 2" меняет X и одновременно меняет другой глобальный ресурс Y, который защищен своим собственным мьютексом. Изменение Y происходит внутри "change2-X" с помощью вызова какой-то функции Y, и работа с мьютексом полностью спрятана внутри этой функции и незаметна при чтении кода thread 2. Тем не менее, эта работа происходит, и если ее учесть, thread 2 выглядит так:

thread 2: get-mutex-for-X ... get-mutex-for-Y ... release-mutex-for-Y change2-X ... release-mutex-for-X

Одно из десятков других мест, которые могут менять X и охраняют это мьютексом, происходит внутри кода, связанного с ресурсом Y, и находящегося глубоко внутри цепочки кода, охраняемой мьютексом для Y. Этот код, выходит, выглядит так:

thread 3: get-mutex-for-Y ... get-mutex-for-X change3-X release-mutex-for-X ... release-mutex-for-Y

Интересно, что изменения change2-X и change3-X никак не конфликтуют друг с другом - они меняют разные места X. Но из-за того, что thread 2 и thread 3 получают два мьютекса для X и Y в разном порядке, получается возможность взаимной блокировки (deadlock). Один поток получил мьютекс для X, и еще до того, как он попытался получить мьютекс для Y, второй поток получил его - и все, они зависают, пытаясь отобрать друг у друга те мьютексы, что у них есть. До того, как я исправил код thread 2, блокировки быть не могло, но теперь она возможна.

Эта самая блокировка и стала происходить, примерно с такой же частотой, как первоначальная проблема падения, т.е. довольно редко, но заметно. Так вышло, что пытаясь починить один сложный многопоточный баг, я ввел другой - причем не что иное, как классический deadlock.

Коллега, который разобрался в этой мешанине и нашел мою ошибку, исправил код thread 2 следующим образом. Он вернул освобождение мьютекса-X перед основной работой этого кода, и вставил эксплицитное ожидание этого мьютекса снова - но только после того, как вся работа с Y закончена. Так что выполнение thread 2 имеет теперь такой вид:

thread 2: get-mutex-for-X ... release-mutex-for-X get-mutex-for-Y ... release-mutex-for-Y get-mutex-for-X change2-X release-mutex-for-X get-mutex-for-X ... release-mutex-for-X.

На данный момент ни о каких новых проблемах с этим кодом не известно.

Date: 2010-08-20 07:42 pm (UTC)
From: [identity profile] dimrub.livejournal.com
У вас есть билд с tsan?

Date: 2010-08-20 07:47 pm (UTC)
From: [identity profile] avva.livejournal.com
Нет - см. выше, думаю, что это для нас нерелевантно, т.к. все происходило при координации C++ с питоном, а питон и его API и SWIG не инструментированы для тсана.

Date: 2010-08-20 10:56 pm (UTC)
From: [identity profile] dimrub.livejournal.com
Хм, это мне немного странно звучит, потому как кажется проблемой, которую кто-то уже должен был решить. В любом случае, билд с tsan-ом - это хорошая идея.

Date: 2010-08-21 05:32 am (UTC)
From: [identity profile] netp-npokon.livejournal.com
Я думаю, аннотировать питон не нужно. Там выше я привел ссылку, где написано, что GIL реализован средствами pthreads, так что перехватится он автоматически.
Дополнительный бонус запуска под валгриндом (будь то tsan, helgrind или даже memcheck) -- то, что при этом гораздо лучше воспроизводятся threading errors. Правда, там и замедлится все соответственно -- из-за глобального лока уже в самом валгринде.

December 2025

S M T W T F S
  123 4 56
78 9 10 11 1213
1415 1617181920
21 22 23 24 2526 27
28293031   

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Dec. 29th, 2025 08:43 am
Powered by Dreamwidth Studios