Thread.Abort() is not supported in .NET 5 / .NET Core
We are actually migrating the NDepend analysis and reporting to .NET 5 and figured out that there is no equivalent for the infamous API Thread.Abort()
. By the time ASP.NET Core was created, CancellationToken
had become the safe and widely accepted alternative to Thread.Abort()
, so there was no need to implement it in the first version of .NET Core. Interestingly enough Thread.Abort()
is a .NET Standard API but it throws NotSupportedException
in .NET 5 / .NET Core context.
Thread.Abort() used to work when carefully handled
There is no discussion: Thread.Abort()
is a dangerous API that throws ThreadAbortException
at any random point deep in the call stack. Nevertheless production logs show that when carefully implemented Thread.Abort()
doesn’t provoke any crash nor state corruption. Careful usage of Thread.Abort()
is not-trivial and implies some events to prevent edge cases to happen. But for more than a decade it worked (and still works) fine for thousands of real users. Till now we applied the tenet if it ain’t broke, don’t fix it.
CancellationToken cannot handle preemptive cancellation scenarios
CancellationToken
is nowadays the safe way to implement cancelable operations. But it is not a replacement for Thread.Abort()
: it only supports co-operative cancellation scenarios, where the cancellable processing is responsible for periodically checking if it has been cancelled.
However CancellationToken
doesn’t support preemptive cancellation scenarios, made possible by the Thread.Abort()
API. Preemptive cancellation is useful to cancel long-running operation involving code that we do not own or that makes awkward to periodically check for cancellation. We have a few such timeout scenarios, the most notable being the execution of code queries compiled on the fly: how to inject periodically checks in a generated LINQ query? We will inject calls to such method CheckTimeOut()
(below) in the generated LINQ query but the whole challenge will be to do it in a way that won’t harm performances:
1 2 3 4 5 6 |
public static T CheckTimeOut(this T t, ICQLinqExecutionContext context) { if(context.CancellationTokenSource.IsCancellationRequested) { throw new TaskCancelledException(); } return t; } |
Eric Lippert advised in this stackoverflow answer to handle such scenario with a dedicated child process that can be safely killed if needed. Unfortunately this suggestion does not apply well to our needs since both the query execution context and the query result aren’t lightweight in many scenarios.
As a consequence some significant work is now required to refactor our tricky but reliable preemptive cancellation implementation into a well-designed co-operative cancellation implementation.
Using CancellationToken to timeout a synchronous processing
Usually CancellationToken
is used when the cancelable operation is processed asynchronously on a worker thread of the CLR thread pool. But in our scenarios the cancellable processing is executed synchronously on the main caller thread. To do so a thread from the CLR pool used to be responsible for calling Thread.Abort()
against the main thread upon timeout. It sounds dangerous but again, when done carefully with some events it works seamlessly.
Hopefully CancellationToken
can be used to timeout a synchronous processing and here is how. Thanks to the CancellationTokenSource(TimeSpan)
constructor this implementation doesn’t even involve any secondary thread to watch for timeout. Undoubtedly this code is way cleaner than anything based on Thread.Abort()
🙂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
using System; using System.Threading; using System.Threading.Tasks; namespace ClassLibraryNetStandard { static class Program { internal static void Main() { bool b = WaitFor.TryCallWithTimeout( OneSecondMethod, 500.ToMilliseconds(), // timeout // 500ms => OneSecondMethod() gets Cancelled // 1500ms => OneSecondMethod() gets Executed out int result); Console.WriteLine($"OneSecondMethod() {(b ? "Executed" : "Cancelled")}"); } static int OneSecondMethod(CancellationToken ct) { for (var i = 0; i < 10; i++) { Thread.Sleep(100.ToMilliseconds()); // co-operative cancellation implies periodically check IsCancellationRequested if (ct.IsCancellationRequested) { throw new TaskCanceledException(); } } return 123; // the result } static TimeSpan ToMilliseconds(this int nbMilliseconds) => new TimeSpan(0, 0, 0, 0, nbMilliseconds); } static class WaitFor { internal static bool TryCallWithTimeout<TResult>( Func<CancellationToken, TResult> proc, TimeSpan timeout, out TResult result) { // Request cancellation after a duration of 'timeout' var cts = new CancellationTokenSource(timeout); try { result = proc(cts.Token); return true; } catch (TaskCanceledException) { } finally { cts.Dispose(); } result = default; return false; } } } |