1
0
Fork 0
mirror of https://github.com/VSadov/Satori.git synced 2025-06-09 09:34:49 +09:00

Implement ControlledExecution API (#71661)

This commit is contained in:
Anton Lapounov 2022-07-27 23:38:38 -07:00 committed by GitHub
parent 93900881bc
commit d365ff2e86
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 516 additions and 13 deletions

View file

@ -100,6 +100,7 @@ The PR that reveals the implementation of the `<IncludeInternalObsoleteAttribute
| __`SYSLIB0043`__ | ECDiffieHellmanPublicKey.ToByteArray() and the associated constructor do not have a consistent and interoperable implementation on all platforms. Use ECDiffieHellmanPublicKey.ExportSubjectPublicKeyInfo() instead. |
| __`SYSLIB0044`__ | AssemblyName.CodeBase and AssemblyName.EscapedCodeBase are obsolete. Using them for loading an assembly is not supported. |
| __`SYSLIB0045`__ | Cryptographic factory methods accepting an algorithm name are obsolete. Use the parameterless Create factory method on the algorithm type instead. |
| __`SYSLIB0046`__ | ControlledExecution.Run method may corrupt the process and should not be used in production code. |
## Analyzer Warnings

View file

@ -208,6 +208,7 @@
<Compile Include="$(BclSourcesRoot)\System\Runtime\CompilerServices\RuntimeFeature.CoreCLR.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\CompilerServices\RuntimeHelpers.CoreCLR.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\CompilerServices\TypeDependencyAttribute.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\ControlledExecution.CoreCLR.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\DependentHandle.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\GCSettings.CoreCLR.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\JitInfo.CoreCLR.cs" />

View file

@ -0,0 +1,159 @@
// 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="System.PlatformNotSupportedException">The method is not supported on this platform.</exception>
/// <exception cref="System.ArgumentNullException">The <paramref name="action"/> argument is null.</exception>
/// <exception cref="System.InvalidOperationException">
/// The current thread is already running the <see cref="ControlledExecution.Run"/> method.
/// </exception>
/// <exception cref="System.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)
{
if (!OperatingSystem.IsWindows())
{
throw new PlatformNotSupportedException();
}
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;
}
}
}
}
}

View file

@ -190,6 +190,7 @@
<Compile Include="System\Resources\ManifestBasedResourceGroveler.NativeAot.cs" />
<Compile Include="System\RuntimeArgumentHandle.cs" />
<Compile Include="System\RuntimeType.cs" />
<Compile Include="System\Runtime\ControlledExecution.NativeAot.cs" />
<Compile Include="System\Runtime\DependentHandle.cs" />
<Compile Include="System\Runtime\CompilerServices\ForceLazyDictionaryAttribute.cs" />
<Compile Include="System\Runtime\CompilerServices\EagerStaticClassConstructionAttribute.cs" />

View file

@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Threading;
namespace System.Runtime
{
public static class ControlledExecution
{
[Obsolete(Obsoletions.ControlledExecutionRunMessage, DiagnosticId = Obsoletions.ControlledExecutionRunDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
public static void Run(Action action, CancellationToken cancellationToken)
{
throw new PlatformNotSupportedException();
}
}
}

View file

@ -20,6 +20,8 @@
#ifdef FEATURE_READYTORUN
IMPORT DynamicHelperWorker
#endif
IMPORT HijackHandler
IMPORT ThrowControlForThread
#ifdef FEATURE_USE_SOFTWARE_WRITE_WATCH_FOR_GC_HEAP
IMPORT g_sw_ww_table
@ -1028,6 +1030,31 @@ FaultingExceptionFrame_FrameOffset SETA SIZEOF__GSCookie
MEND
; ------------------------------------------------------------------
;
; Helpers for ThreadAbort exceptions
;
NESTED_ENTRY RedirectForThreadAbort2,,HijackHandler
PROLOG_SAVE_REG_PAIR fp,lr, #-16!
; stack must be 16 byte aligned
CHECK_STACK_ALIGNMENT
; On entry:
;
; x0 = address of FaultingExceptionFrame
;
; Invoke the helper to setup the FaultingExceptionFrame and raise the exception
bl ThrowControlForThread
; ThrowControlForThread doesn't return.
EMIT_BREAKPOINT
NESTED_END RedirectForThreadAbort2
GenerateRedirectedStubWithFrame RedirectForThreadAbort, RedirectForThreadAbort2
; ------------------------------------------------------------------
; ResolveWorkerChainLookupAsmStub
;

View file

@ -154,6 +154,24 @@ $FuncName
MEND
;-----------------------------------------------------------------------------
; Macro used to check (in debug builds only) whether the stack is 16-bytes aligned (a requirement before calling
; out into C++/OS code). Invoke this directly after your prolog (if the stack frame size is fixed) or directly
; before a call (if you have a frame pointer and a dynamic stack). A breakpoint will be invoked if the stack
; is misaligned.
;
MACRO
CHECK_STACK_ALIGNMENT
#ifdef _DEBUG
add x9, sp, xzr
tst x9, #15
beq %F0
EMIT_BREAKPOINT
0
#endif
MEND
;-----------------------------------------------------------------------------
; The Following sets of SAVE_*_REGISTERS expect the memory to be reserved and
; base address to be passed in $reg

View file

@ -918,12 +918,6 @@ PTR_CONTEXT GetCONTEXTFromRedirectedStubStackFrame(T_CONTEXT * pContext)
return *ppContext;
}
void RedirectForThreadAbort()
{
// ThreadAbort is not supported in .net core
throw "NYI";
}
#if !defined(DACCESS_COMPILE)
FaultingExceptionFrame *GetFrameFromRedirectedStubStackFrame (DISPATCHER_CONTEXT *pDispatcherContext)
{

View file

@ -9,4 +9,9 @@ extern "C"
{
PORTABILITY_ASSERT("Implement for PAL");
}
void RedirectForThreadAbort()
{
PORTABILITY_ASSERT("Implement for PAL");
}
};

View file

@ -1096,15 +1096,38 @@ extern "C" BOOL QCALLTYPE ThreadNative_YieldThread()
BOOL ret = FALSE;
BEGIN_QCALL
BEGIN_QCALL;
ret = __SwitchToThread(0, CALLER_LIMITS_SPINNING);
END_QCALL
END_QCALL;
return ret;
}
extern "C" void QCALLTYPE ThreadNative_Abort(QCall::ThreadHandle thread)
{
QCALL_CONTRACT;
BEGIN_QCALL;
thread->UserAbort(EEPolicy::TA_Safe, INFINITE);
END_QCALL;
}
// Unmark the current thread for a safe abort.
extern "C" void QCALLTYPE ThreadNative_ResetAbort()
{
QCALL_CONTRACT_NO_GC_TRANSITION;
Thread *pThread = GetThread();
if (pThread->IsAbortRequested())
{
pThread->UnmarkThreadForAbort(EEPolicy::TA_Safe);
}
}
FCIMPL0(INT32, ThreadNative::GetCurrentProcessorNumber)
{
FCALL_CONTRACT;

View file

@ -103,6 +103,8 @@ extern "C" void QCALLTYPE ThreadNative_InformThreadNameChange(QCall::ThreadHandl
extern "C" UINT64 QCALLTYPE ThreadNative_GetProcessDefaultStackSize();
extern "C" BOOL QCALLTYPE ThreadNative_YieldThread();
extern "C" UINT64 QCALLTYPE ThreadNative_GetCurrentOSThreadId();
extern "C" void QCALLTYPE ThreadNative_Abort(QCall::ThreadHandle thread);
extern "C" void QCALLTYPE ThreadNative_ResetAbort();
#endif // _COMSYNCHRONIZABLE_H

View file

@ -209,6 +209,8 @@ static const Entry s_QCall[] =
DllImportEntry(ThreadNative_InformThreadNameChange)
DllImportEntry(ThreadNative_YieldThread)
DllImportEntry(ThreadNative_GetCurrentOSThreadId)
DllImportEntry(ThreadNative_Abort)
DllImportEntry(ThreadNative_ResetAbort)
DllImportEntry(ThreadPool_GetCompletedWorkItemCount)
DllImportEntry(ThreadPool_RequestWorkerThread)
DllImportEntry(ThreadPool_PerformGateActivities)

View file

@ -2496,7 +2496,7 @@ private:
public:
void MarkThreadForAbort(EEPolicy::ThreadAbortTypes abortType);
void UnmarkThreadForAbort();
void UnmarkThreadForAbort(EEPolicy::ThreadAbortTypes abortType = EEPolicy::TA_Rude);
static ULONGLONG GetNextSelfAbortEndTime()
{

View file

@ -1785,7 +1785,7 @@ void Thread::RemoveAbortRequestBit()
}
// Make sure that when AbortRequest bit is cleared, we also dec TrapReturningThreads count.
void Thread::UnmarkThreadForAbort()
void Thread::UnmarkThreadForAbort(EEPolicy::ThreadAbortTypes abortType /* = EEPolicy::TA_Rude */)
{
CONTRACTL
{
@ -1794,11 +1794,14 @@ void Thread::UnmarkThreadForAbort()
}
CONTRACTL_END;
// Switch to COOP (for ClearAbortReason) before acquiring AbortRequestLock
GCX_COOP();
AbortRequestLockHolder lh(this);
if (m_AbortType > (DWORD)abortType)
{
// Aborting at a higher level
return;
}
m_AbortType = EEPolicy::TA_None;
m_AbortEndTime = MAXULONGLONG;
m_RudeAbortEndTime = MAXULONGLONG;

View file

@ -147,5 +147,8 @@ namespace System
internal const string CryptoStringFactoryMessage = "Cryptographic factory methods accepting an algorithm name are obsolete. Use the parameterless Create factory method on the algorithm type instead.";
internal const string CryptoStringFactoryDiagId = "SYSLIB0045";
internal const string ControlledExecutionRunMessage = "ControlledExecution.Run method may corrupt the process and should not be used in production code.";
internal const string ControlledExecutionRunDiagId = "SYSLIB0046";
}
}

View file

@ -2500,6 +2500,10 @@
<data name="InvalidOperation_NativeOverlappedReused" xml:space="preserve">
<value>NativeOverlapped cannot be reused for multiple operations.</value>
</data>
<data name="InvalidOperation_NestedControlledExecutionRun" xml:space="preserve">
<value>The thread is already executing the ControlledExecution.Run method.</value>
<comment>{Locked="ControlledExecution.Run"}</comment>
</data>
<data name="InvalidOperation_NoMultiModuleAssembly" xml:space="preserve">
<value>You cannot have more than one dynamic module in each dynamic assembly in this version of the runtime.</value>
</data>

View file

@ -12136,6 +12136,11 @@ namespace System.Runtime
public AssemblyTargetedPatchBandAttribute(string targetedPatchBand) { }
public string TargetedPatchBand { get { throw null; } }
}
public static partial class ControlledExecution
{
[System.ObsoleteAttribute("ControlledExecution.Run method may corrupt the process and should not be used in production code.", DiagnosticId = "SYSLIB0046", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")]
public static void Run(System.Action action, System.Threading.CancellationToken cancellationToken) { throw null; }
}
public partial struct DependentHandle : System.IDisposable
{
private object _dummy;

View file

@ -226,6 +226,7 @@
<Compile Include="System\Reflection\TypeDelegatorTests.cs" />
<Compile Include="System\Reflection\TypeTests.Get.CornerCases.cs" />
<Compile Include="System\Reflection\TypeTests.GetMember.cs" />
<Compile Include="System\Runtime\ControlledExecutionTests.cs" />
<Compile Include="System\Runtime\DependentHandleTests.cs" />
<Compile Include="System\Runtime\JitInfoTests.cs" />
<Compile Include="System\Runtime\MemoryFailPointTests.cs" />

View file

@ -0,0 +1,221 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Threading;
using Xunit;
// Disable warnings for ControlledExecution.Run
#pragma warning disable SYSLIB0046
namespace System.Runtime.Tests
{
public class ControlledExecutionTests
{
private bool _startedExecution, _caughtException, _finishedExecution;
private Exception _exception;
private CancellationTokenSource _cts;
private volatile int _counter;
// Tests cancellation on timeout. The ThreadAbortException must be mapped to OperationCanceledException.
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)]
public void CancelOnTimeout()
{
var cts = new CancellationTokenSource();
cts.CancelAfter(200);
RunTest(LengthyAction, cts.Token);
Assert.True(_startedExecution);
Assert.True(_caughtException);
Assert.False(_finishedExecution);
Assert.IsType<OperationCanceledException>(_exception);
}
// Tests that catch blocks are not aborted. The action catches the ThreadAbortException and throws an exception of a different type.
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)]
public void CancelOnTimeout_ThrowFromCatch()
{
var cts = new CancellationTokenSource();
cts.CancelAfter(200);
RunTest(LengthyAction_ThrowFromCatch, cts.Token);
Assert.True(_startedExecution);
Assert.True(_caughtException);
Assert.False(_finishedExecution);
Assert.IsType<TimeoutException>(_exception);
}
// Tests that finally blocks are not aborted. The action throws an exception from a finally block.
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)]
public void CancelOnTimeout_ThrowFromFinally()
{
var cts = new CancellationTokenSource();
cts.CancelAfter(200);
RunTest(LengthyAction_ThrowFromFinally, cts.Token);
Assert.True(_startedExecution);
Assert.IsType<TimeoutException>(_exception);
}
// Tests that finally blocks are not aborted. The action throws an exception from a try block.
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)]
public void CancelOnTimeout_Finally()
{
var cts = new CancellationTokenSource();
cts.CancelAfter(200);
RunTest(LengthyAction_Finally, cts.Token);
Assert.True(_startedExecution);
Assert.True(_finishedExecution);
Assert.IsType<TimeoutException>(_exception);
}
// Tests cancellation before calling the Run method
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)]
public void CancelBeforeRun()
{
var cts = new CancellationTokenSource();
cts.Cancel();
Thread.Sleep(100);
RunTest(LengthyAction, cts.Token);
Assert.IsType<OperationCanceledException>(_exception);
}
// Tests cancellation by the action itself
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)]
public void CancelItself()
{
_cts = new CancellationTokenSource();
RunTest(Action_CancelItself, _cts.Token);
Assert.True(_startedExecution);
Assert.False(_finishedExecution);
Assert.IsType<AggregateException>(_exception);
Assert.IsType<ThreadAbortException>(_exception.InnerException);
}
private void RunTest(Action action, CancellationToken cancellationToken)
{
_startedExecution = _caughtException = _finishedExecution = false;
_exception = null;
try
{
ControlledExecution.Run(action, cancellationToken);
}
catch (Exception e)
{
_exception = e;
}
}
private void LengthyAction()
{
_startedExecution = true;
// Redirection via thread suspension is supported on Windows only.
// Make a call in the loop to allow redirection on other platforms.
bool sleep = !PlatformDetection.IsWindows;
try
{
for (_counter = 0; _counter < int.MaxValue; _counter++)
{
if ((_counter & 0xfffff) == 0 && sleep)
{
Thread.Sleep(0);
}
}
}
catch
{
// Swallow all exceptions to verify that the exception is automatically rethrown
_caughtException = true;
}
_finishedExecution = true;
}
private void LengthyAction_ThrowFromCatch()
{
_startedExecution = true;
bool sleep = !PlatformDetection.IsWindows;
try
{
for (_counter = 0; _counter < int.MaxValue; _counter++)
{
if ((_counter & 0xfffff) == 0 && sleep)
{
Thread.Sleep(0);
}
}
}
catch
{
_caughtException = true;
// The catch block must not be aborted
Thread.Sleep(100);
throw new TimeoutException();
}
_finishedExecution = true;
}
private void LengthyAction_ThrowFromFinally()
{
_startedExecution = true;
try
{
// Make sure to run the non-inlined finally
throw new Exception();
}
finally
{
// The finally block must not be aborted
Thread.Sleep(400);
throw new TimeoutException();
}
}
private void LengthyAction_Finally()
{
_startedExecution = true;
try
{
// Make sure to run the non-inlined finally
throw new TimeoutException();
}
finally
{
// The finally block must not be aborted
Thread.Sleep(400);
_finishedExecution = true;
}
}
private void Action_CancelItself()
{
_startedExecution = true;
try
{
// Make sure to run the non-inlined finally
throw new TimeoutException();
}
finally
{
// The finally block must be aborted
_cts.Cancel();
_finishedExecution = true;
}
}
}
}

View file

@ -254,6 +254,7 @@
<Compile Include="$(BclSourcesRoot)\System\Reflection\Metadata\AssemblyExtensions.cs" />
<Compile Include="$(BclSourcesRoot)\System\Reflection\Metadata\MetadataUpdater.cs" />
<Compile Include="$(BclSourcesRoot)\System\Resources\ManifestBasedResourceGroveler.Mono.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\ControlledExecution.Mono.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\DependentHandle.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\GCSettings.Mono.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\JitInfo.Mono.cs" />

View file

@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Threading;
namespace System.Runtime
{
public static class ControlledExecution
{
[Obsolete(Obsoletions.ControlledExecutionRunMessage, DiagnosticId = Obsoletions.ControlledExecutionRunDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
public static void Run(Action action, CancellationToken cancellationToken)
{
throw new PlatformNotSupportedException();
}
}
}