Поаккуратнее с топором. Часть 1: Нужно ли мне указывать таймаут?
Не так давно, примерно шесть лет назад, я рассказывал немного о том, как решить, стоит ли продолжать ждать автобус или бросить это дело и пойти пешком. Эта статья привела к весьма интересному обсуждению на старом форуме Джоэла Спольски (Joel Spolsky). Но что если выбор состоит не в том, чтобы «подождать немного и сдаться», а в том, чтобы «подождать немного, а затем прибить поток топором»? Я периодически сталкиваюсь со сценарием, который выглядит примерно так: я создаю и запускаю рабочий поток, прошу его о завершении и ожидаю некоторое время до его завершения. Если он не завершается через определенное время, я беру топор и прибиваю его:
this.running = false;
if (!workerThread.Join(timeout))
workerThread.Abort();
Насколько хороша эта идея?
Это зависит от того, насколько плохо ведет себя рабочий поток и что он делает, в случае его некорректного поведения.
Если вы можете гарантировать, что длительность рабочей операции небольшая, чтобы это для вас ни значило, тогда тайм-аут вам вообще не нужен. Если же вы этого гарантировать не можете, тогда, прежде всего, я предлагаю переписать ваш код, чтобы вы смогли это гарантировать. Жизнь становится значительно проще, если вы знаете, что код завершится быстро, после того, как вы попросите его об этом.
Что же делать, если вы не можете этого сделать? В этом случае предполагаем, что поведение рабочего потока является некорректным, и он не завершится за отведенное время после того, как его попросят о завершении. Теперь нам нужно задать себе один вопрос: «является ли долгое завершение потока преднамеренным, ошибочным или злонамеренным?
В первом случае, рабочий поток просто выполняет некоторую длительную операцию, которая по какой-то причине не может быть прервана. Как правильно поступить в этом случае? Понятия не имею. Это ужасная ситуация. Вероятно, рабочий поток быстро не завершает свою работу, поскольку это опасно или невозможно. Что вы собираетесь делать в этом случае после истечения тайм-аута? Вы получите поток, который опасно или невозможно прервать и он не будет завершен своевременно. В этом случае у вас есть несколько вариантов:
(1) ничего не предпринимать
(2) подождать еще
(3) сделать нечто невозможное. Лучше всего перед завтраком.
(4) сделать нечто опасное
Первый вариант эквивалентный тому, чтобы вообще не ждать завершения. Если именно это вы и собираетесь делать, то зачем было вообще начинать ожидание? Второй вариант сводится к изменению тайм-аута на другое значение. Спорный вариант, если вы не собираетесь ожидать бесконечное время. Третий вариант невозможен. Остается «сделать нечто опасное», что звучит … опасно.
Правильный выбор, с точки зрения минимизации вреда пользовательским данным, зависит от конкретных обстоятельств, способных привести к опасным последствиям. Тщательно проанализируйте эти обстоятельства, поймите все возможные сценарии и определите правильный вариант решения. Здесь нет простого решения. Оно будет полностью зависеть от конкретного исполняемого кода.
Предположим, что рабочий поток предусматривает возможность быстрого завершения, но не делает это из-за ошибки. Очевидно, стоит исправить ошибку, если вы можете это сделать. Если вы не можете исправить ошибку (возможно, она находится не в вашем коде) тогда, опять же, вы находитесь в ужасной ситуации. Вы должны понять последствия того, что не будете ожидать завершения ошибочного-кода-с-непредсказуемым-поведением, прежде чем освобождать ресурсы, которые, как вы знаете, используются в другом потоке. И вы должны выяснить последствия прекращения ошибочного рабочего потока, в тот момент, когда он все еще выполняет неизвестно что, с точки зрения состояния операционной системы.
Если код является злонамеренным и активно сопротивляется своему завершению, вы уже проиграли. Вы не можете нормальным образом остановить выполнение потока, и даже не можете надежным образом прервать его выполнение. Нет гарантий того, что прекращение работы злонамеренного потока действительно приведет к его остановке. Владелец злонамеренного кода, выполнение которого вы по глупости запустили в своем процессе, сможет выполнить всю необходимую работу в блоках finally или в другой ограниченной области (constrained region), которые предотвращают генерацию исключения.
Лучшее, что можно сделать в этом случае, так это, прежде всего, постараться не попадать в такую ситуацию. Если у вас есть код, который вы считаете злонамеренным, тогда, либо не запускайте его совсем, либо запускайте в отдельном процессе и, в случае неприятностей, прерывайте процесс, а не поток.
Одним словом не существует правильного ответа на вопрос «Что же мне делать, когда завершение потока длится слишком долго?» В этом случае вы оказываетесь в ужасной ситуации, из которой нет простого выхода. Прежде всего, нужно приложить все усилия, чтобы предотвратить такую ситуацию и запускать только отзывчивый, безвредный, безопасный код, который по первому требованию прекращает свое выполнение, не оставляя следов. Евгений, осторожнее с топором.
А как насчет исключений? Поговорим об этом в следующий раз.