Thursday, June 2, 2022

Calling Thread.Abort and Thread.ResetAbort several times

In this short article I want to analyze a situation when we want to call Thread.Abort several times for a thread which uses Thread.ResetAbort to control cancellation process.

Let's start with a simple code to understand how Thread.Abort works. I'll create a thread with an infinite loop and then I'll abort it:

using System;
using System.Threading;

namespace ThreadAbort
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(Run);
            thread.Start();

            Console.WriteLine("Press any key to abort the thread.");
            Console.ReadKey();

            thread.Abort();
        }

        static void Run()
        {
            while(true) { }
        }
    }
}

The Run method is executed in a separate thread. When Abort is called for this thread the runtime throws special ThreadAbortException inside the thread. The thread code can catch this exception and gracefully handle thread abortion.

But does it mean that I can swallow this exception and continue the thread execution? Let's see.

using System;
using System.Threading;

namespace ThreadAbort
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(Run);
            thread.Start();

            Console.WriteLine("Press any key to abort the thread.");
            Console.ReadKey();

            thread.Abort();
        }

        static void Run()
        {
            try
            {
                while (true) { }
            }
            catch (ThreadAbortException)
            {
                Console.WriteLine("Thread is aborted");
            }

            Console.WriteLine("Continue execution");
        }
    }
}

Here we catch a ThreadAbortException and continue execution. But the line printing Continue execution is never called. You see, when all catch and finally blocks are finished, the runtime rethrows the ThreadAbortException. This is why all code after try-catch is never executed.

But what if I do want to continue the execution of the thread code? In this case, I must use Thread.ResetAbort:

using System;
using System.Threading;

namespace ThreadAbort
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(Run);
            thread.Start();

            Console.WriteLine("Press any key to abort the thread.");
            Console.ReadKey();

            thread.Abort();
        }

        static void Run()
        {
            try
            {
                while (true) { }
            }
            catch (ThreadAbortException)
            {
                Console.WriteLine("Thread is aborted");
                Thread.ResetAbort();
            }

            Console.WriteLine("Continue execution");
        }
    }
}

Here I call Thread.ResetAbort inside my catch block. It means, that I don't want ThreadAbortException to be rethrown. This is why this time the line Continue execution is printed.

But recently I came across the following situation. I have a thread method that constructs a complicated page for rendering. If this process takes too long, I abort the process and construct another page with error message using the same method but with another state. I can illustrate this situation with the following code:

static void Run()
{
    try
    {
        while (true) { }
    }
    catch (ThreadAbortException)
    {
        Thread.ResetAbort();
        
        State = "error";

        Run();
    }
}

But it appeared that even creation of the error page can take too long. In this case, I want to abort the thread again and now construct really simple page. So, what is the problem? Let's see. Here is a sketch of my code:

using System;
using System.Threading;

namespace ThreadAbort
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(Run);
            thread.Start();

            while (true)
            {
                Console.WriteLine("Press any key to abort the thread.");
                Console.ReadKey();

                thread.Abort();
            }
        }

        static void Run()
        {
            Console.WriteLine("Thread execution started");

            try
            {
                while (true) { }
            }
            catch (ThreadAbortException)
            {
                Console.WriteLine("Thread is aborted");
                Thread.ResetAbort();

                Run();
            }
        }
    }
}

The Run method recursively calls itself on every abort. The while loop in the Main method allows me to abort the thread arbitrary number of times. But when you run this program it appears that actually you can abort the thread only once. Why?

The reason is in the try-catch block. You see, while the code is still inside the catch block, system thinks that the process of aborting is not finished yet. This is why it ignores all consecutive calls of Thread.Abort. And we invoke the Run method from inside the catch block. This is why we never leave it.

So, what should we do to be able to call Thread.Abort several times? We should move the recursive invocation of the Run method outside of the catch block:

using System;
using System.Threading;

namespace ThreadAbort
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(Run);
            thread.Start();

            while (true)
            {
                Console.WriteLine("Press any key to abort the thread.");
                Console.ReadKey();

                thread.Abort();
            }
        }

        static void Run()
        {
            Console.WriteLine("Thread execution started");

            try
            {
                while (true) { }
            }
            catch (ThreadAbortException)
            {
                Console.WriteLine("Thread is aborted");
                Thread.ResetAbort();
            }

            Run();
        }
    }
}

Now we can abort the thread as many times as we want.

I hope this little advice is useful for you. Good luck!

No comments:

Post a Comment