Предположив, что аварийно завершаемый поток не будет вызывать 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 – если вслед за ним последует завершение домена приложений или процесса. |