|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | + |
| 4 | +using System.Runtime.CompilerServices; |
| 5 | +using System.Runtime.ExceptionServices; |
| 6 | +using System.Runtime.InteropServices; |
| 7 | +using System.Threading; |
| 8 | + |
| 9 | +namespace System.Runtime |
| 10 | +{ |
| 11 | + /// <summary> |
| 12 | + /// Allows to run code and abort it asynchronously. |
| 13 | + /// </summary> |
| 14 | + public static partial class ControlledExecution |
| 15 | + { |
| 16 | + [ThreadStatic] |
| 17 | + private static bool t_executing; |
| 18 | + |
| 19 | + /// <summary> |
| 20 | + /// Runs code that may be aborted asynchronously. |
| 21 | + /// </summary> |
| 22 | + /// <param name="action">The delegate that represents the code to execute.</param> |
| 23 | + /// <param name="cancellationToken">The cancellation token that may be used to abort execution.</param> |
| 24 | + /// <exception cref="System.PlatformNotSupportedException">The method is not supported on this platform.</exception> |
| 25 | + /// <exception cref="System.ArgumentNullException">The <paramref name="action"/> argument is null.</exception> |
| 26 | + /// <exception cref="System.InvalidOperationException"> |
| 27 | + /// The current thread is already running the <see cref="ControlledExecution.Run"/> method. |
| 28 | + /// </exception> |
| 29 | + /// <exception cref="System.OperationCanceledException">The execution was aborted.</exception> |
| 30 | + /// <remarks> |
| 31 | + /// <para>This method enables aborting arbitrary managed code in a non-cooperative manner by throwing an exception |
| 32 | + /// in the thread executing that code. While the exception may be caught by the code, it is re-thrown at the end |
| 33 | + /// of `catch` blocks until the execution flow returns to the `ControlledExecution.Run` method.</para> |
| 34 | + /// <para>Execution of the code is not guaranteed to abort immediately, or at all. This situation can occur, for |
| 35 | + /// example, if a thread is stuck executing unmanaged code or the `catch` and `finally` blocks that are called as |
| 36 | + /// part of the abort procedure, thereby indefinitely delaying the abort. Furthermore, execution may not be |
| 37 | + /// aborted immediately if the thread is currently executing a `catch` or `finally` block.</para> |
| 38 | + /// <para>Aborting code at an unexpected location may corrupt the state of data structures in the process and lead |
| 39 | + /// to unpredictable results. For that reason, this method should not be used in production code and calling it |
| 40 | + /// produces a compile-time warning.</para> |
| 41 | + /// </remarks> |
| 42 | + [Obsolete(Obsoletions.ControlledExecutionRunMessage, DiagnosticId = Obsoletions.ControlledExecutionRunDiagId, UrlFormat = Obsoletions.SharedUrlFormat)] |
| 43 | + public static void Run(Action action, CancellationToken cancellationToken) |
| 44 | + { |
| 45 | + if (!OperatingSystem.IsWindows()) |
| 46 | + { |
| 47 | + throw new PlatformNotSupportedException(); |
| 48 | + } |
| 49 | + |
| 50 | + ArgumentNullException.ThrowIfNull(action); |
| 51 | + |
| 52 | + // ControlledExecution.Run does not support nested invocations. If there's one already in flight |
| 53 | + // on this thread, fail. |
| 54 | + if (t_executing) |
| 55 | + { |
| 56 | + throw new InvalidOperationException(SR.InvalidOperation_NestedControlledExecutionRun); |
| 57 | + } |
| 58 | + |
| 59 | + // Store the current thread so that it may be referenced by the Canceler.Cancel callback if one occurs. |
| 60 | + Canceler canceler = new(Thread.CurrentThread); |
| 61 | + |
| 62 | + try |
| 63 | + { |
| 64 | + // Mark this thread as now running a ControlledExecution.Run to prevent recursive usage. |
| 65 | + t_executing = true; |
| 66 | + |
| 67 | + // Register for aborting. From this moment until ctr.Unregister is called, this thread is subject to being |
| 68 | + // interrupted at any moment. This could happen during the call to UnsafeRegister if cancellation has |
| 69 | + // already been requested at the time of the registration. |
| 70 | + CancellationTokenRegistration ctr = cancellationToken.UnsafeRegister(e => ((Canceler)e!).Cancel(), canceler); |
| 71 | + try |
| 72 | + { |
| 73 | + // Invoke the caller's code. |
| 74 | + action(); |
| 75 | + } |
| 76 | + finally |
| 77 | + { |
| 78 | + // This finally block may be cloned by JIT for the non-exceptional code flow. In that case the code |
| 79 | + // below is not guarded against aborting. That is OK as the outer try block will catch the |
| 80 | + // ThreadAbortException and call ResetAbortThread. |
| 81 | + |
| 82 | + // Unregister the callback. Unlike Dispose, Unregister will not block waiting for an callback in flight |
| 83 | + // to complete, and will instead return false if the callback has already been invoked or is currently |
| 84 | + // in flight. |
| 85 | + if (!ctr.Unregister()) |
| 86 | + { |
| 87 | + // Wait until the callback has completed. Either the callback is already invoked and completed |
| 88 | + // (in which case IsCancelCompleted will be true), or it may still be in flight. If it's in flight, |
| 89 | + // the AbortThread call may be waiting for this thread to exit this finally block to exit, so while |
| 90 | + // spinning waiting for the callback to complete, we also need to call ResetAbortThread in order to |
| 91 | + // reset the flag the AbortThread call is polling in its waiting loop. |
| 92 | + SpinWait sw = default; |
| 93 | + while (!canceler.IsCancelCompleted) |
| 94 | + { |
| 95 | + ResetAbortThread(); |
| 96 | + sw.SpinOnce(); |
| 97 | + } |
| 98 | + } |
| 99 | + } |
| 100 | + } |
| 101 | + catch (ThreadAbortException tae) |
| 102 | + { |
| 103 | + // We don't want to leak ThreadAbortExceptions to user code. Instead, translate the exception into |
| 104 | + // an OperationCanceledException, preserving stack trace details from the ThreadAbortException in |
| 105 | + // order to aid in diagnostics and debugging. |
| 106 | + OperationCanceledException e = cancellationToken.IsCancellationRequested ? new(cancellationToken) : new(); |
| 107 | + if (tae.StackTrace is string stackTrace) |
| 108 | + { |
| 109 | + ExceptionDispatchInfo.SetRemoteStackTrace(e, stackTrace); |
| 110 | + } |
| 111 | + throw e; |
| 112 | + } |
| 113 | + finally |
| 114 | + { |
| 115 | + // Unmark this thread for recursion detection. |
| 116 | + t_executing = false; |
| 117 | + |
| 118 | + if (cancellationToken.IsCancellationRequested) |
| 119 | + { |
| 120 | + // Reset an abort request that may still be pending on this thread. |
| 121 | + ResetAbortThread(); |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ThreadNative_Abort")] |
| 127 | + private static partial void AbortThread(ThreadHandle thread); |
| 128 | + |
| 129 | + [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ThreadNative_ResetAbort")] |
| 130 | + [SuppressGCTransition] |
| 131 | + private static partial void ResetAbortThread(); |
| 132 | + |
| 133 | + private sealed class Canceler |
| 134 | + { |
| 135 | + private readonly Thread _thread; |
| 136 | + private volatile bool _cancelCompleted; |
| 137 | + |
| 138 | + public Canceler(Thread thread) |
| 139 | + { |
| 140 | + _thread = thread; |
| 141 | + } |
| 142 | + |
| 143 | + public bool IsCancelCompleted => _cancelCompleted; |
| 144 | + |
| 145 | + public void Cancel() |
| 146 | + { |
| 147 | + try |
| 148 | + { |
| 149 | + // Abort the thread executing the action (which may be the current thread). |
| 150 | + AbortThread(_thread.GetNativeHandle()); |
| 151 | + } |
| 152 | + finally |
| 153 | + { |
| 154 | + _cancelCompleted = true; |
| 155 | + } |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | +} |
0 commit comments