Обработка исключений в многопоточных приложениях
13К открытий13К показов
Рассказывает Пол Шарф, автор блога genericgamedev.com
В прошлом уроке мы использовали многопоточность для создания анимированных (или даже интерактивных) экранов загрузки, а также для уменьшения времени загрузки в целом.
И хотя мы рассмотрели множество вещей на примере Roche Fusion, кое-что мы не затронули вовсе — что, если что-то пойдет не так? Или, более техническим языком — что, если один из наших потоков вернет исключение?
Если мы ничего не будем с этим делать, то поток, который вернет исключение (например, из-за бага или испорченного файла), завершится без нашего ведома, и мы никогда об этом не узнаем.
Так происходит, потому что исключения постепенно накапливаются в стеке вызовов до тех пор, пока не будут обработаны или не заполнят весь стек. В последнем случае поток, в котором появилось исключение, завершает работу.
Если проблема происходит в главном потоке, то пользователь просто увидит ошибку, но если речь идет о других потоках, то они при исключении завершаются без единого следа, и программа может повиснуть, ожидая успешного завершения работы аварийно остановившегося потока.
Заметьте, что ниже описанные решения этой проблемы применимы к любым многопоточным приложениям. Загрузочные экраны служат лишь в качестве примера.
Возможные решения
Существует несколько возможных способов обрабатывать исключения в потоках.
Например, главный поток может регулярно проверять, остановились ли другие и сделали ли они что-нибудь с возникшим исключением.
Проблема этого решения в том, что нам нужно быть в курсе всех выполняемых потоков, в ином случае какой-нибудь поток все равно экстренно завершится, не подав вида.
Альтернативно мы можем подписаться на глобальные события, происходящие во всем приложении, которые будут информировать нас о произошедших исключениях, так, что мы сможем реагировать на них.
Но и здесь не все так гладко. Такой подход не позволит нам «потерять» потоки, но из-за асинхронной природы приложения мы не можем быть уверены в том, когда исполнится код нашего обработчика, тем самым рискуя натолкнуться на другие проблемы многопоточности.
Используем то, что у нас уже есть
Ранее мы расписали архитектуру небольшого фреймворка, и я бы хотел представить вам решение, которое не приводит к вышеописанным проблемам.
В приложении, написанном нами в прошлом уроке, любой поток мог давать главному потоку работу на выполнение. Каждый поток может сам обрабатывать исключения таким образом, что все возникшие ошибки передавались бы главному потоку. В таком случае исключение либо заставит упасть все приложение, либо будет обработано должным образом. Вот простой пример реализации такой идеи:
actionQueue
– это объект класса, разработанного нами в этом посте, который позволяет перенаправлять из всех потоков какие-либо действия на главный поток.
Таким образом, когда threadAction
возвращает исключение, оно не обрабатывается, а помещается в очередь на исполнение в главном потоке.
Когда главный поток доберется до исключения в очереди действий, он уже обработает его как надо, а если не сможет, то приложение просто закончит работу.
Данный подход отлично работает на первый взгляд, но есть одна проблема, которая очень быстро становится явной при отладке — при возвращении исключения, которое уже возвращалось ранее, его стек вызовов автоматически перезаписывается. И без начального стека вызовов мы даже не сможем определить, в какой части программы возникло исключение.
Сохраняем стеки вызовов
К счастью, у этой проблемы есть очень легкое решение.
Вместо того, чтобы возвращать то же исключение, что и раньше, мы можем просто создать новое, содержащее в себе старое, как это делает C#.
Внесем поправку в написанный выше код:
И это все, что нам нужно, чтобы убедиться в том, что мы не теряем информацию о предыдущем исключении при возвращении такого же нового.
Дополнительно
Конечно, если возможно, то лучше проектировать ваше приложение так, что возможность возникновения фатального исключения стремится к нулю.
Например, мы в Roche Fusion оборачиваем каждый скриптовый файл в try-catch
. В случае, если что-то пойдет не так, в игровую консоль выводится предупреждение и загрузка продолжается.
В большинстве случаев это никак не влияет на игру, разве что некоторый контент может отображаться не так, как надо.
Однажды мы все-таки не обратили внимание на одно из предупреждений и напоролись на ситуацию, в которой при определенных обстоятельствах приложение могло крашиться из-за того, что один скрипт не загружался. Но в основном такой подход намного облегчает разработку и моддинг.
Заключение
Надеюсь, что эта статья дала вам представление о том, как надо обрабатывать возникающие исключения в многопоточных приложениях.
Если вас заинтересовали другие подходы, которые я упомянул, но о которых не рассказал подробнее, рекомендую поискать их онлайн.
Наслаждайтесь пикселями!
13К открытий13К показов