Сложности с Thread.Abort

Предположив, что аварийно завершаемый поток не будет вызывать ResetAbort, можно было бы ожидать, что убийство произойдет довольно быстро. Но, как это обычно случается, с хорошим адвокатом можно провести в камере смертников довольно долгое время! Вот несколько факторов, которые могут задержать поток в состоянии AbortRequested:

§ Статические конструкторы классов не могут быть принудительно прерваны на середине (чтобы не испортить класс, необходимый для дальнейшей работы домена приложения).

§ Все блоки catch/finally не могут быть принудительно завершены на полпути.

§ Если принудительно завершаемый поток исполняет в это время неуправляемый код, его выполнение продолжается до первой инструкции управляемого кода.

Последний фактор особенно неприятен, так как.NET Framework часто вызывает неуправляемый код, иногда оставаясь там в течение долгого времени. Примером может быть работа с сетью или базами данных. Если сетевой ресурс или сервер баз данных умирают или не спешат с ответом, выполнение может оставаться в неуправляемом коде в течение минут, в зависимости от используемой реализации. В этих случаях было бы неприятно зависнуть на Join, ожидая завершения потока, особенно без использования таймаута.

Принудительное завершение работы чистого.NET-кода менее проблематично, если для гарантии надлежащей очистки после выброса ThreadAbortException используются блоки try/finally или конструкция using. Однако и в этом случае нужно быть готовым к неприятным сюрпризам. Посмотрите, например, на следующий код:

using(StreamWriter w = File.CreateText("myfile.txt")) w.Write("Abort-Safe?");

C#-оператор using – на самом деле просто сокращенная запись для следующего кода:

StreamWriter w; w = File.CreateText("myfile.txt"); try { w.Write("Abort-Safe"); } finally { w.Dispose(); }

Abort может случиться после создания StreamWriter, но перед началом блока try. Фактически, углубляясь в IL, можно увидеть, что это может произойти даже между созданием StreamWriter и присвоением значения w:

IL_0001: ldstr "myfile.txt" IL_0006: call class [mscorlib]System.IO.StreamWriter [mscorlib]System.IO.File::CreateText(string) IL_000b: stloc.0 .try { ...

В этом случае до вызова Dispose в блоке finally дело не дойдет, хэндл открытого файла зависнет, пресекая любые попытки создать myfile.txt в течение оставшейся жизни домена приложения.

В действительности ситуация еще хуже, так как Abort может иметь место внутри реализации File.CreateText. Исходные коды этого метода официально не открыты, но к счастью, в.NET трудно что-либо действительно закрыть, можно снова обратиться к ILDASM, – и заглянув соответствующую сборку, увидеть, что там вызывается конструктор StreamWriter, который имеет следующую логику:

public StreamWriter(string path, bool append,...) { ... ... Stream stream1 = StreamWriter.CreateFile(path, append); this.Init(stream1,...); }

Нигде в этом конструкторе нет блоков try/catch, а это значит, что вызов Abort во время выполнения нетривиального метода Init подвесит только что созданный StreamWriter безо всякого способа закрыть принадлежащий ему файловый хэндл.

Поскольку дизассемблирование каждого используемого вызова CLR, очевидно, непрактично, встает вопрос, как же писать методы, которые можно без проблем аварийно завершать. Самый очевидный выход из ситуации – не завершать аварийно другие потоки вообще, а использовать специальное булево поле, через которое сигнализировать потоку о необходимости завершения. Рабочий поток должен периодически проверять это поле, элегантно завершаясь, если оно установлено в true. Как ни странно, самым элегантным завершением для рабочего потока будет вызов Abort для себя самого, подойдет также явный выброс исключения. Это гарантирует правильную очистку потоков в процессе выполнения блоков catch/finally – аналогично вызову Abort из другого потока, за исключением того, что исключение генерируется в выбранном нами месте:

class ProLife { public static void Main() { RulyWorker w = new RulyWorker(); Thread t = new Thread(w.Work); t.Start(); Thread.Sleep(500); w.Abort(); } public class RulyWorker { // Ключевое слово volatile гарантирует, что abort не будет // кешироваться потоком volatile bool abort; public void Abort() { abort = true; } public void Work() { while (true) { CheckAbort(); // Делаем что-то полезное... try { OtherMethod(); } finally { /* требуемая очистка */ } } } void OtherMethod() { // Делаем что-то полезное... CheckAbort(); } void CheckAbort() { if (abort) Thread.CurrentThread.Abort(); } } }
Вызов Abort для текущего потока – один из вариантов, при которых Abort будет полностью безопасен. Другой вариант – точное знание, что прерываемый извне поток находится в определенной секции кода, обычно достигаемое использованием механизмов синхронизации типа WaitHandle или Monitor.Wait. Третий безопасный вариант вызова Abort – если вслед за ним последует завершение домена приложений или процесса.

Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: