2

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.

Patrick from NDepend team
  • 13,237
  • 6
  • 61
  • 92

2 Answers2

4

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.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • No unfortunately the abortable code cannot be changed and thus cannot periodically check for CancellationToken. Unfortunately it looks like the Thread.Abort() feature is not implementable in .NET Core/.NET5, is it? – Patrick from NDepend team May 11 '21 at 14:48
  • 1
    @PatrickfromNDependteam You are going to *have* to change your code to do cooperative cancellation, and really, you shouldn't be using `Thread.Abort` anyway, it has huge side-effects due to throwing random exceptions deep in the call stack. I would have thought someone like you, having coded a major .NET took, would know that. – Charlieface May 11 '21 at 15:13
  • @Charlieface sure I know all of the theory, but in practice production logs show that we are using Thread.Abort() in a safe way (thanks to some events), it doesn't provoke any random crash nor corrupted state – Patrick from NDepend team May 12 '21 at 04:06
  • @Patrick suggestion: you could use a sub-process based model and kill the entire process – Marc Gravell May 12 '21 at 06:01
  • Thanks @MarcGravell we thought about this option but discard it so far. We concluded that investing now in (significant) dev for a co-operative cancellation makes sense in the long run, thanks – Patrick from NDepend team May 12 '21 at 15:14
1

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;
      }
   }
}
Patrick from NDepend team
  • 13,237
  • 6
  • 61
  • 92