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.

На данный момент ни о каких новых проблемах с этим кодом не известно.
Page 1 of 3 << [1] [2] [3] >>

Date: 2010-08-20 01:50 am (UTC)
From: [identity profile] creaturen2.livejournal.com
По нашей профессии тоже можно снимать фильм наподобие "Доктора Хауса".

Date: 2010-08-20 01:56 am (UTC)
From: [identity profile] kot-begemot.livejournal.com
Когда-то давным-давно был такой дивный инструмент для статического анализа многопоточного кода - locklint. Отлавливал такие ситуации довольно хорошо.
Решение коллеги, кстати, не идеально. Я бы, наверное, попытался разделить изменение X и Y по времени, более того, сделал бы доступ к полям X и Y исключительно через аксессоры/мутаторы, в которых бы и брал нужные мютексы.
Помимо всего прочего, это могло бы увеличить эффективность за счёт более высокой ганулярности сериализаций.

Date: 2010-08-20 01:57 am (UTC)
From: [identity profile] itman.livejournal.com
Это будет совершенно не гламурно. И потом, никто не хочет быть программистом, все хотят быть врачами.
From: [identity profile] larisaka.livejournal.com
Фраза "Падений больше не было. Вскоре я уехал в отпуск." как-то выпадает из стиля записи, вам не кажется? какая-то литературная. Программисты могут огорчиться.

Date: 2010-08-20 02:11 am (UTC)
From: [identity profile] itman.livejournal.com
Во многих ситуациях такое временное освобождение мьютекса как раз правильное поведение - если бы change2-X не меняло X, а делало какие-то совершенное другие вещи, и притом не очень быстро, то как раз это правильно. Но в данном случае это ошибка.

Кстати, ИМХО, это очень часто ошибка. Потому что код до первого освобождения мьютекса, обычно проверяет какое-то важное предусловие для последующего изменения. И, если это предусловие не проверяется повторно после "финального взятия" мьютекса, случаются чудеса.

Date: 2010-08-20 03:01 am (UTC)
From: [identity profile] raindog-2.livejournal.com
По совпадению, я как раз только что прослушал tech talk насчет STM в Closure (http://www.infoq.com/presentations/Value-Identity-State-Rich-Hickey). Твою историю можно запускать как страшилку перед рекламой Clojure & friends. "Раньше я пользовался Мьютексом, и страдал изжогой, а теперь перешел на persistent data structures / managed references, и мои волосы опять стали пушистыми" :)

Date: 2010-08-20 03:17 am (UTC)
From: [identity profile] http://users.livejournal.com/_navi_/
There's a reason movies never portray hacking realistically. (http://www.smbc-comics.com/index.php?db=comics&id=1898#comic)

Date: 2010-08-20 03:23 am (UTC)
From: [identity profile] once-for-all.livejournal.com
Multithreading 101 С одной стороны - rookie mistake; с другой - the bitch to debug, тем более в чужом коде..

Из любпытство - насколько необходим общий mutex для всего Х? Была ли возможность заменить на, скажем, атомик переменные, или скажем все изменения такого рода посылаются в background thread? Если бы вы начинали делать новый дизайн.

Date: 2010-08-20 04:13 am (UTC)
From: [identity profile] cema.livejournal.com
Ну да, только не перешел.

Date: 2010-08-20 04:15 am (UTC)
From: [identity profile] cema.livejournal.com
А почему разные треды берут мьютексы в разном порядке? Непорядок.

Date: 2010-08-20 04:27 am (UTC)
From: [identity profile] rezkiy.livejournal.com
мне как-то Арун Кишан рассказывал, как он автоматизировал процесс поиска таких ошибок без собственно поиска дедлоков. Ясно дело, что если дедлокнуло, то дампы говорят сами за себя. Но иногда дедлокает редко, и есть риск шипнуть с багом.

Date: 2010-08-20 05:07 am (UTC)
From: [identity profile] iratus.livejournal.com
Почему был? Он и сейчас прекрасно есть. Но работает только под Солярисом.
Для Линуксов есть Helgrind

Date: 2010-08-20 05:08 am (UTC)
From: [identity profile] iratus.livejournal.com
Это далеко не rookie mistake.

Date: 2010-08-20 05:31 am (UTC)
stas: (Don't panic!)
From: [personal profile] stas
Вот поэтому threads are evil.

Date: 2010-08-20 06:15 am (UTC)
lxe: (Default)
From: [personal profile] lxe
Не, зло - это блокировки. А рулят атомарные операции.

Date: 2010-08-20 06:17 am (UTC)
lxe: (Default)
From: [personal profile] lxe
Накапливал и сравнивал snapshots стека блокировок в разных потоках и в разные моменты выполнения?

Date: 2010-08-20 06:38 am (UTC)
From: [identity profile] once-for-all.livejournal.com
мы же не про индийцев, пишущих Oracle applications говорим, а про серверы на С++. Тут в принципе другой уровень.
Но на этом уровне deadlock - классическая ошибка с которой все начинают

Date: 2010-08-20 06:39 am (UTC)
From: [identity profile] once-for-all.livejournal.com
+1000. Queue it!

Date: 2010-08-20 06:46 am (UTC)
From: [identity profile] rezkiy.livejournal.com
Рассказываю что помню:
1. ну ясное дело врапперы для инициализации, взятия и отпускания всех локов
2. тред-локальная переменная, считающая локи которые взяты тредом (в его случае это были спинлоки, поэтому переменная процессор-локальная).
3. как и везде, каждый лок имеет Номер В Очереди. Просто здесь он явно указывается при инициализации. Локи с одинаковым номером одновременно не берутся никогда (например, это локи на бакеты хеш-таблицы, зачем благородным донам два бакета одновременно???). Если вдруг берется лок, а лок с таким же или большим номером в очереди на данном треде уже взят -- валим. Если оказалось что первоначальные предположения о порядке оказались неверными -- меняем номера как правильно, запускаем тест заново.
4. Из-за оверхеда с номерами, чтобы быстро работало, количество слоев в иерархии локов должно быть небольшим, например 32. Я так понял что чуваку хватило.

ну, в результате оно будет валиться когда локи берутся не в том порядке, вместо того, чтобы повисать когда локи на одном ядре берутся в том порядке и одновременно на другом ядре -- не в том.

Ну я как бы подумал, городить ли мне такое в том проекте над которым я тогда работал, но потом понял, что у меня все настолько проще (типа, три слоя иерархии, ERESOURCE и два спинлока под ним), что игра не стоит свеч. ERESOURCE я точно после спинлока взять никак не могу :-).

Date: 2010-08-20 06:49 am (UTC)
From: [identity profile] rezkiy.livejournal.com
А в чем была Ваша мысль про сборку стектрейсов? Я честно не понял, как оно может помочь для поиска дедлоков.

Date: 2010-08-20 06:51 am (UTC)
From: [identity profile] tejblum.livejournal.com
В ядре FreeBSD, например, есть отладочная фича под названием "WITNESS". Когда берется мьютекс Y при уже взятом мьютексе X, этот факт запоминается, и если потом кто-то берет мьютекс X при взятом мьютексе Y, печатается диагностика...

Date: 2010-08-20 06:54 am (UTC)
From: [identity profile] rezkiy.livejournal.com
вставьте элемент в двунаправленный список атомарной операцией, тогда поговорим.

Date: 2010-08-20 06:59 am (UTC)
From: [identity profile] netp-npokon.livejournal.com
А вы не пробовали использовать ThreadSanitizer для поиска не защищенных мьютексом доступов? Если нет, то это, по-видимому, наша недоработка :)

Date: 2010-08-20 07:06 am (UTC)
From: [identity profile] migmit.vox.com (from livejournal.com)
Да уж. STM рулит и бибикает.

Date: 2010-08-20 07:11 am (UTC)
From: [identity profile] rezkiy.livejournal.com
http://msdn.microsoft.com/en-us/library/ff543668(VS.85).aspx
Page 1 of 3 << [1] [2] [3] >>

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 06:40 am
Powered by Dreamwidth Studios