Code Review: Dlaczego nie należy korzystać z Thread.Abort

Kilka wpisów wcześniej pisałem, dlaczego należy unikać funkcji Suspend. Dzisiaj przyszła kolej na metodę Abort, która również jest sygnałem, że zaprojektowana architektura jest po prostu zła. Aby zrozumieć, dlaczego Abort jest tak niebezpieczny, należy poznać najpierw zasadę jego działania. Wywołując Abort, wyrzucany jest tzw. asynchroniczny wyjątek ThreadAbortException. Dlaczego asynchroniczny? Ponieważ może on zostać wstrzyknięty w “dowolne” miejsce w kodzie. Istnieją pewne zasady, kiedy dokładnie może on zostać wyrzucony ale o tym później. Najpierw spróbujmy udowodnić, że faktycznie takowy wyjątek istnieje:

internal class Program
{
   private static void Main(string[] args)
   {
       Task.Factory.StartNew(Run);
       Console.Read();
   }
   private static void Run()
   {
       while (true)
       {
           Console.WriteLine("Iteracja...");
           try
           {
               Thread.CurrentThread.Abort();
           }
           catch (ThreadAbortException e)
           {
               Console.WriteLine("Zlapano ThreadAbortException");
           }
       }
   }
}

Powyższa funkcja złapie wyjątek. ThreadAbortException jest specjalnym wyjątkiem ponieważ jest wyrzucany asynchronicznie oraz nie da się go zignorować – automatycznie  jest zawsze ponownie wyrzucony przez CLR. Pomimo, że mamy catch i tak wątek będzie zatrzymany. Na końcu bowiem, zawsze jest rethrow. Aby anulować Abort, należy wywołać funkcję ResetAbort – wtedy wyjątek nie będzie ponownie wyrzucany:

internal class Program
{
   private static void Main(string[] args)
   {
       Task.Factory.StartNew(Run);
       Console.Read();
   }
   private static void Run()
   {
       while (true)
       {
           Console.WriteLine("Iteracja...");
           try
           {
               try
               {
                   Thread.CurrentThread.Abort();
               }
               catch (ThreadAbortException e)
               {
                   Console.WriteLine("Zlapano ThreadAbortException");
               }
           }
           catch(ThreadAbortException e)
           {
               Console.WriteLine("Wyjatek ponownie został wyrzucony.");
               Thread.ResetAbort();
           }
       }
   }
}

Powyższy drugi catch, zostanie wywołany ponieważ jak napisałem, CLR automatycznie zrobi rethrow. Abort jest bardziej inteligentniejszy od Suspend i nie zostanie wyrzucony wyjątek jeśli aktualnie wykonywany jest kod:

  1. wewnątrz CER,
  2. wewnątrz CATCH lub FINALLY,
  3. niezarządzany,
  4. w lock (kiedyś były problemy z tym ale teraz podobno jest już to naprawione),
  5. w konstruktorze.

W takich przypadkach, wątek zostanie dopiero przerwany, gdy IP opuści te klauzule.  W praktyce jednak istnieje wiele niebezpiecznych sytuacji np.:

IntPtr pointer = AllocateUnmanagedCode();
try
{
    // jakas logika
}
finally
{
    Free(pointer);
}

 

Co w przypadku gdy wyjątek zostanie wyrzucony po alokacji zasobów ale przed przypisaniem ich do pointer? Zasoby nigdy nie zostaną zwolnione. Klauzula finally oczywiście nie zostanie wywołana ani destruktor IntPtr. Problem z abort jest taki, że nie wiadomo kiedy zostanie wywołany. Jeśli stanie się to w sytuacji gdy np. otwieramy plik, ale przed przypisaniem do wskaźnika, wtedy plik zostanie otwarty mimo, że wątek już nie pracuje. Bardzo trudno byłoby zaprojektować kod odporny na ThreadAbortException i z tego względu należy unikać asynchronicznych wyjątków.

Leave a Reply

Your email address will not be published.