Use .NET 5 CancellationToken to call a method that can timeout on the main thread

Viewed 471

In .NET Fx 4 we used to use this approach described here to timeout a method call. This approach relies on Thread.Abort() that doesn't work in .NET 5 as explained here "It may come as a surprise that Thread.Abort was never implemented for .NET Core".

What is super useful in this approach is that the called method (the one that might timeout) is executed on the main thread, the same thread used to execute the caller method that initiates the call. Before this call, an async thread is fired to wait during the timeout interval and then eventually call Thread.Abort() on the main thread that catches ThreadAbortException and then in the catch handler calls Thread.ResetAbort().

How can we have the same behavior with some .NET Core / .NET 5 code?

This Microsoft doc Cancel async tasks after a period of time shows something different where the method that can timeout is called on an async thread and not on the main thread.

2 Answers

CancellationToken is co-operative cancellation; if you can change the offending code to periodically check the token to see whether it should give up, then: just do that. However, you can't just add a CancellationToken to code and expect it to start acting (when triggered) similarly to Thread.Abort(); that is very different, in that it actively interrupts execution without requiring any co-operation from the code that is being interrupted.

There are two points:

First Preemptive cancellation implemented through Thread.Abort() / ThreadAbortException in .NET Fx is not possible anymore in .NET Core/.NET5. As @Marc Gravel explains Co-operative cancellation (where the cancellable method periodically checks if it has been cancelled) must be used instead. As a consequence scenarios where code to cancel is in a black box that doesn't accept a CancellationToken cannot be implemented in .NET Core.

Second it is possible to run the cancellable method on the main thread without even involving a pool thread. Here is .NET Standard code sample that works both .NET Fx and .NET Core/.NET5 platforms:

using System;
using System.Threading;
using System.Threading.Tasks;
namespace ClassLibraryNetStandard {
   static class CancellationTokenTest_OpOnMainThread {
      internal static void Go() {
         bool b = WaitFor<int>.TryCallWithTimeout(
            500.ToMilliseconds(), // timeout
            OneSecondMethod,
            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: periodically check if CancellationRequested
            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<TResult> {
      internal static bool TryCallWithTimeout(
            TimeSpan timeout,
            Func<CancellationToken, TResult> proc,
            out TResult result) {
         var cts = new CancellationTokenSource(timeout);
         try {
            result = proc(cts.Token);
            return true;
         } catch (TaskCanceledException) { }
         finally { cts.Dispose(); }
         result = default;
         return false;
      }
   }
}
Related