![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Многопоточные грабли
Q: Is SQLite threadsafe?
A: Threads are evil. Avoid them.
SQLite is threadsafe. We make this concession since many users choose to ignore the advice given in the previous paragraph.
«Часто задаваемые вопросы по SQLite»
Жутко не люблю многопоточность. И полностью согласен с автором SQLite, чьё мнение по этому поводу приведено в эпиграфе. Потоки — зло.
Конечно, можно сказать, что я просто не умею их готовить. И я с этим даже соглашусь. Поскольку готовил их довольно часто, а в результате обычно получалась малосъедобная гадость, которая вроде и работает, но иногда как учудит что-нибудь этакое!
Повторюсь, потоки — зло. Но иногда — зло необходимое.
Хотя сами по себе потоки безобидны. Когда они, допустим, живут по отдельности и ни с чем не взаимодействуют. Отработал до конца, вернул результат и закрылся. Это я понимаю.
А вот ежели потоков несколько и они пытаются где-то дёргать что-то постороннее, тут и начинается веселье. Отладка хитрого переплетения потоков — занятие не для слабонервных. Радует, что современные технологии отладки хорошо продвинулись вперёд, по сравнению с моим институтским опытом пятнадцатилетней давности. Но иногда и этого не хватает.
Вот давеча подкинули мне ошибочку. Она сама по себе простая и неинтересная: в один прекрасный момент в логах появляется сообщение, дескать, нету у такого-то контрола, который захотел себя отрисовать, родительского компонента. Ну, думаю, раз нету, значит кто-то его создал вручную, а родителя не назначил. Щас разберёмся.
Полез разбираться. Нетушки, не вручную его создали. Прописан он на форме с самого начала. Значит, пропала куда-то эта форма. А как она могла пропасть, ежели контрол ещё живой? Форма же вначале все свои компоненты удаляет, и только потом — себя. Что за фокусы?
Полез разбираться глубже и выяснил интересную штуку. Заодно, кстати, узнал много нового про многопоточность в Delphi.
Итак, есть у нас на форме такой невизуальный компонент: TThreadSafeTimer
(потокобезопасный таймер), унаследованный от стандартного дельфийского TThread
. Задача у него простая, он просыпается раз в N секунд и смотрит, пользуется ли кто-нибудь ресурсом? Если уже давно не пользуется, то ресурс и освободить можно, пусть другие попользуются.
Это у нас был первый персонаж. А вот второй: где-то в другом потоке некто хочет вывести на экран какое-то сообщение. Для этого он, как принято в Delphi, берёт свой метод, выводящий сообщение, и вызывает его через Synchronize
. Потому что безопасно работать с контролами может только главный поток, а Synchronize
как раз и предназначен, чтобы исполнять методы в главном потоке.
И вот мизансцена: Synchronize
вызван, а в этот момент начинается Армагеддон. Форма, на которой у нас расположен таймер и на которую собираются выводить сообщение, начинает уничтожаться. Главным потоком, который всему голова.
Разумеется, прежде чем скончаться, форма перебирает всё, что на ней находится, и планомерно уничтожает. И доходит до нашего таймера.
Таймер, как мы помним, унаследован от TThread
. Следовательно, в унаследованном деструкторе есть вызов WaitFor
, предназначенный для корректного завершения потока. А в этом самом WaitFor
есть вызов CheckSynchronize
, в котором как раз всё самое интересное.
Тут надо сделать отступление и объяснить, как работает Synchronize
. Было бы ошибкой думать, что в момент вызова Synchronize
прервёт работу главного потока и сразу начнёт исполнять от его имени то, что мы передали. Ничего подобного. Главный поток в момент вызова может заниматься другими, крайне важными делами. И прерывать его нельзя.
Поэтому Synchronize
делает следующее. Он берёт метод, который ему передали на исполнение, и засовывает его в специальный список, после чего останавливает вызвавший поток. А позже, когда главный поток освободится, он сей список проверит, увидит что там что-то есть, возьмёт оттуда метод, исполнит его, а потом запустит поток заново. С того же места, где он был остановлен. И всё чудесно.
Но есть ещё один нюанс — WaitFor
. Это такой метод, который позволяет одному потоку подождать завершения другого. То есть, поток А вызывает WaitFor
для потока Б и тем самым останавливается до тех пор, пока Б не завершит свою работу. Теперь представим себе, что главный поток решил подождать, пока не завершится поток А, а тот в свою очередь вызвал Synchronize
. И мы получаем взаимную блокировку: основной поток остановлен и не может взять очередной метод из списка синхронизации, а поток А остановлен, пока не выполнится синхронизированный метод.
Поэтому в начале WaitFor
стоит вызов CheckSynchronize
. Эта штука просто вытаскивает из списка синхронизации очередной отложенный метод — и исполняет. И так далее, пока степь не кончится, то есть список не опустеет. И вот когда в списке уж точно не осталось синхронизированных методов — тогда и подождать можно.
А теперь вернёмся к нашей ситуации. Форма наполовину уничтожилась, связи на ней разорваны, может уже и контрола нету, на который планировалось сообщение выводить. Но синхронизированный метод лежит себе в списке синхронизации, как в криокамере, и не знает, что там снаружи огненный дождь и стены рушатся. И тут деструктор потокобезопасного таймера вытаскивает его из глубокой заморозки наружу и оживляет. Тот пытается по старой памяти сообщение вывести, как собирался, но на прежнем месте уже контрола нету, а есть какие-то ошмётки. Которые, как ни странно, даже принимают переданное им поручение что-то вывести, но выводить-то уже некуда!
Такая вот трагичная история, которую мне пришлось раскапывать несколько часов. Зато интересно.