I take Exception to that! Exception Handling in multi-threaded code


Far and away improper implementation of exception handlers can be the most insidious source of threading mistakes. Unless the application author implements proper exception handling/handlers, an uncaught exception at minimum can cause a live lock or deadlock, or worst case can terminate the application without cleaning up background threads. In addition, an unhandled exception can leak resources. e.g. in the .NET Framework or Core, Dispose methods will not be called.

Here are some common mistakes I've seen:

1) Failure to install application wide AND app domain exception handlers. As noted above, an unhandled exception can cause thread lock or crash the application leaving orphaned threads and resources.
using System;

public class Example
{
   public static void Main()
   {
      AppDomain currentDomain = AppDomain.CurrentDomain;
      currentDomain.UnhandledException += new  
UnhandledExceptionEventHandler(MyHandler); try { throw new Exception("1"); } catch (Exception e) { Console.WriteLine("Catch clause caught : {0} \n", e.Message); } throw new Exception("2"); } static void MyHandler(object sender, UnhandledExceptionEventArgs args) { Exception e = (Exception) args.ExceptionObject; Console.WriteLine("MyHandler caught : " + e.Message); Console.WriteLine("Runtime terminating: {0}", args.IsTerminating); } } // The example displays the following output: // Catch clause caught : 1 // // MyHandler caught : 2 // Runtime terminating: True // // Unhandled Exception: System.Exception: 2 //

2) Improper implementation of exception filtering. In this example, exception specific catch handlers that are either implemented in the wrong order, or are implemented without a default handler can lead to similar issues, particularly where there are no custom application and app domain handlers implemented.
using System;

public class Example
{
   public static void Main()
   {
      try 
      {
         throw new ArithmeticException (...);
      } 
      catch (ArgumentOutOfRangeException e) 
      {
         Console.WriteLine("Catch clause caught : {0} \n", e.Message);
      } 
   }
}
// The example will not catch the ArithmeticException exception and will cause the application
// to terminate

3) The Task Parallel Library (TPL) uses AggregateException to combine multiple failures into a single, throwable exception object.

Here's how AggregateException works in TPL:
* If a child task throws an exception, it's wrapped in an AggregateException before it's passed to the parent task.
* The parent task then wraps the exception in its own AggregateException before passing it back to the calling thread.

If the AggregateException is not handled or improperly implemented, then the same issue of live locks/deadlocks/application termination will occur.
public static void Main()
{
    try
    {
        Parallel.For(0, 500000, i =>
        {
            if (i == 10523)
                throw new TimeoutException("i = 10523");
            Console.WriteLine(i + "\n");
        });
    }
    catch (AggregateException exception)
    {
        foreach (Exception ex in exception.InnerExceptions)
        {
            Console.WriteLine(ex.ToString());
        }
    }
}

4) Mixing Structured Exception Handling with language specific exception handling. Best practices would dictate to choose one or the other (e.g. C++, C#). Mixing both types can lead to unexpected code paths and logic flow.
#include <iostream>
<h1>include <Windows.h></h1>

void raise_exception()
{
    try 
    {
        throw std::exception{};
    } catch (...) 
    {
        RaiseException(0x1337, 1, 0, 0);
    }
}

void do_example() 
{
    __try 
    {
        raise_exception();
    } __except (EXCEPTION_EXECUTE_HANDLER) {}
}

int main()
{
    try 
    {
        try 
        {
            do_example();
            throw std::exception{};
        } catch (...) {}
    } catch (...) 
    {
        std::cout << "unexpected" << std::endl;
    }
    return 0;
}

In this code, a non-C++ SEH is thrown from within a C++ catch (in raise_exception), then caught by a SEH __try and swallowed (in do_example).
Afterwards, a C++ exception is thrown (in main), but within two identical C++ try-blocks.
The expected behavior is for the exception to be caught and handled in the inner catch-block.
The actual behavior is that it is caught by the outer catch-block, and the string "unexpected" is printed.

I'm sure there are other use cases, my goal here is to give a taste and pitfalls to watch out for.
And so it goes...

Comments

Popular posts from this blog

Representing C/C++ unions and bitfields in C#

Implementing the try/catch/finally pattern in C++ using lambdas, or why cant this be as easy as it is in C#?

Dispatch? I'd like to make a call. The difference between Synchronization Context, Task Scheduler and Dispatcher