File: src\System\Runtime\ControlledExecution.CoreCLR.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Threading;
 
namespace System.Runtime
{
    /// <summary>
    /// Allows to run code and abort it asynchronously.
    /// </summary>
    public static partial class ControlledExecution
    {
        [ThreadStatic]
        private static bool t_executing;
 
        /// <summary>
        /// Runs code that may be aborted asynchronously.
        /// </summary>
        /// <param name="action">The delegate that represents the code to execute.</param>
        /// <param name="cancellationToken">The cancellation token that may be used to abort execution.</param>
        /// <exception cref="PlatformNotSupportedException">The method is not supported on this platform.</exception>
        /// <exception cref="ArgumentNullException">The <paramref name="action"/> argument is null.</exception>
        /// <exception cref="InvalidOperationException">
        /// The current thread is already running the <see cref="Run"/> method.
        /// </exception>
        /// <exception cref="OperationCanceledException">The execution was aborted.</exception>
        /// <remarks>
        /// <para>This method enables aborting arbitrary managed code in a non-cooperative manner by throwing an exception
        /// in the thread executing that code.  While the exception may be caught by the code, it is re-thrown at the end
        /// of `catch` blocks until the execution flow returns to the `ControlledExecution.Run` method.</para>
        /// <para>Execution of the code is not guaranteed to abort immediately, or at all.  This situation can occur, for
        /// example, if a thread is stuck executing unmanaged code or the `catch` and `finally` blocks that are called as
        /// part of the abort procedure, thereby indefinitely delaying the abort.  Furthermore, execution may not be
        /// aborted immediately if the thread is currently executing a `catch` or `finally` block.</para>
        /// <para>Aborting code at an unexpected location may corrupt the state of data structures in the process and lead
        /// to unpredictable results.  For that reason, this method should not be used in production code and calling it
        /// produces a compile-time warning.</para>
        /// </remarks>
        [Obsolete(Obsoletions.ControlledExecutionRunMessage, DiagnosticId = Obsoletions.ControlledExecutionRunDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
        public static void Run(Action action, CancellationToken cancellationToken)
        {
            ArgumentNullException.ThrowIfNull(action);
 
            // ControlledExecution.Run does not support nested invocations.  If there's one already in flight
            // on this thread, fail.
            if (t_executing)
            {
                throw new InvalidOperationException(SR.InvalidOperation_NestedControlledExecutionRun);
            }
 
            // Store the current thread so that it may be referenced by the Canceler.Cancel callback if one occurs.
            Canceler canceler = new(Thread.CurrentThread);
 
            try
            {
                // Mark this thread as now running a ControlledExecution.Run to prevent recursive usage.
                t_executing = true;
 
                // Register for aborting.  From this moment until ctr.Unregister is called, this thread is subject to being
                // interrupted at any moment.  This could happen during the call to UnsafeRegister if cancellation has
                // already been requested at the time of the registration.
                CancellationTokenRegistration ctr = cancellationToken.UnsafeRegister(e => ((Canceler)e!).Cancel(), canceler);
                try
                {
                    // Invoke the caller's code.
                    action();
                }
                finally
                {
                    // This finally block may be cloned by JIT for the non-exceptional code flow.  In that case the code
                    // below is not guarded against aborting.  That is OK as the outer try block will catch the
                    // ThreadAbortException and call ResetAbortThread.
 
                    // Unregister the callback.  Unlike Dispose, Unregister will not block waiting for an callback in flight
                    // to complete, and will instead return false if the callback has already been invoked or is currently
                    // in flight.
                    if (!ctr.Unregister())
                    {
                        // Wait until the callback has completed.  Either the callback is already invoked and completed
                        // (in which case IsCancelCompleted will be true), or it may still be in flight.  If it's in flight,
                        // the AbortThread call may be waiting for this thread to exit this finally block to exit, so while
                        // spinning waiting for the callback to complete, we also need to call ResetAbortThread in order to
                        // reset the flag the AbortThread call is polling in its waiting loop.
                        SpinWait sw = default;
                        while (!canceler.IsCancelCompleted)
                        {
                            ResetAbortThread();
                            sw.SpinOnce();
                        }
                    }
                }
            }
            catch (ThreadAbortException tae)
            {
                // We don't want to leak ThreadAbortExceptions to user code.  Instead, translate the exception into
                // an OperationCanceledException, preserving stack trace details from the ThreadAbortException in
                // order to aid in diagnostics and debugging.
                OperationCanceledException e = cancellationToken.IsCancellationRequested ? new(cancellationToken) : new();
                if (tae.StackTrace is string stackTrace)
                {
                    ExceptionDispatchInfo.SetRemoteStackTrace(e, stackTrace);
                }
                throw e;
            }
            finally
            {
                // Unmark this thread for recursion detection.
                t_executing = false;
 
                if (cancellationToken.IsCancellationRequested)
                {
                    // Reset an abort request that may still be pending on this thread.
                    ResetAbortThread();
                }
            }
        }
 
        [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ThreadNative_Abort")]
        private static partial void AbortThread(ThreadHandle thread);
 
        [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ThreadNative_ResetAbort")]
        [SuppressGCTransition]
        private static partial void ResetAbortThread();
 
        private sealed class Canceler
        {
            private readonly Thread _thread;
            private volatile bool _cancelCompleted;
 
            public Canceler(Thread thread)
            {
                _thread = thread;
            }
 
            public bool IsCancelCompleted => _cancelCompleted;
 
            public void Cancel()
            {
                try
                {
                    // Abort the thread executing the action (which may be the current thread).
                    AbortThread(_thread.GetNativeHandle());
                }
                finally
                {
                    _cancelCompleted = true;
                }
            }
        }
    }
}