From 863afd57ea69c7fcbf8886c13bd8dff339da6690 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Fri, 8 May 2026 23:38:09 +0200 Subject: [PATCH 1/2] Cache continuation used for runtime async callable value task thunks `ValueTask` backed by `IValueTaskSource` can suspend without allocating. However, runtime async did not support this when calling a `ValueTask` returning method through a runtime async callable thunk. In that case we would always allocate in the case where the `ValueTask` suspended. This adds a custom `ValueTaskContinuation` that replaces the existing `ValueTaskSourceNotifier` mechanism. We now cache a `ValueTaskContinuation` instance and reuse it if possible whenever suspending for a `ValueTask`. The same approach could be used to avoid allocations for runtime async callable thunks for task-returning methods. --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 214 +++++++++++++++++- src/coreclr/debug/daccess/dacdbiimpl.cpp | 2 +- src/coreclr/inc/dacvars.h | 1 + .../Common/TypeSystem/IL/Stubs/AsyncThunks.cs | 23 +- src/coreclr/vm/asynccontinuations.cpp | 2 - src/coreclr/vm/asyncthunks.cpp | 28 +-- src/coreclr/vm/corelib.h | 7 +- src/coreclr/vm/methodtable.cpp | 11 +- src/coreclr/vm/methodtable.h | 3 +- src/coreclr/vm/typehandle.cpp | 9 +- src/coreclr/vm/typehandle.h | 1 + src/coreclr/vm/typehandle.inl | 2 +- src/coreclr/vm/vars.cpp | 2 + src/coreclr/vm/vars.hpp | 2 + .../src/System/Threading/Tasks/ValueTask.cs | 80 ------- 15 files changed, 270 insertions(+), 117 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs index bb068f1290c0f8..d66a34d883a59a 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs @@ -125,6 +125,16 @@ public ref byte GetResultStorageOrNull() ref byte data = ref RuntimeHelpers.GetRawData(this); return ref Unsafe.Add(ref data, (DataOffset - PointerSize) + index * PointerSize); } + + protected void EncodeFieldOffsetInFlags(ref byte field, ContinuationFlags firstBit, ContinuationFlags numBits) + { + int offset = (int)Unsafe.ByteOffset(ref RuntimeHelpers.GetRawData(this), ref field); + offset -= DataOffset; + Debug.Assert(offset >= 0 && offset % PointerSize == 0); + uint index = 1 + (uint)offset / PointerSize; + Debug.Assert(index < (1 << (int)numBits)); + Flags |= (ContinuationFlags)((uint)index << (int)firstBit); + } } [StructLayout(LayoutKind.Explicit)] @@ -202,7 +212,7 @@ private ref struct RuntimeAsyncStackState // to one of these notifiers. public ICriticalNotifyCompletion? CriticalNotifier; public INotifyCompletion? Notifier; - public ValueTaskSourceNotifier? ValueTaskSourceNotifier; + public ValueTaskContinuation? ValueTaskContinuation; public Task? TaskNotifier; // When we suspend in the leaf, the contexts are captured into these fields. @@ -245,6 +255,7 @@ public void Pop(Thread thread) private unsafe struct RuntimeAsyncAwaitState { public Continuation? SentinelContinuation; + public ValueTaskContinuation? CachedValueTaskContinuation; // We cache the thread here to avoid unnecessary repeated TLS lookups. public Thread? CurrentThread; @@ -330,6 +341,200 @@ private static unsafe Continuation AllocContinuationClass(Continuation prevConti } #endif + private sealed unsafe class ValueTaskContinuation : Continuation + { + // Currently all continuations are expected to capture and restore + // ExecutionContext, even though we do not actually need it here. + public ExecutionContext? ExecutionContext; + private object? _source; + private short _token; + private delegate* managed, object?, short, ValueTaskSourceOnCompletedFlags, void> _onCompleted; + private delegate* managed _getResult; + + public ValueTaskContinuation() + { + ResumeInfo = (ResumeInfo*)Unsafe.AsPointer(ref ValueTaskContinuationResume.ResumeInfo); + + EncodeFieldOffsetInFlags( + ref Unsafe.As(ref ExecutionContext), + ContinuationFlags.ExecutionContextIndexFirstBit, + ContinuationFlags.ExecutionContextIndexNumBits); + } + + public void OnCompleted(Action continuation, object? state, ValueTaskSourceOnCompletedFlags flags) + { + Debug.Assert(_source != null); + _onCompleted(_source, continuation, state, _token, flags); + } + + public void GetResult(ref byte returnValue) + { + Debug.Assert(_source != null); + + // Avoid retaining source. The call below may throw. + object source = _source; + _source = null; + + _getResult(source, _token, ref returnValue); + } + + public void Initialize(object source, short token) + { + _source = source; + _token = token; + _onCompleted = &OnCompleted; + _getResult = &GetResult; + } + + public void Initialize(object source, short token) + { + _source = source; + _token = token; + _onCompleted = &OnCompleted; + _getResult = &GetResult; + } + + private static void OnCompleted(object source, Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + { + if (source is Task t) + { + Debug.Assert(state is ITaskCompletionAction); + if (!t.TryAddCompletionAction(Unsafe.As(ref state))) + { + ThreadPool.UnsafeQueueUserWorkItemInternal(state, preferLocal: true); + } + } + else + { + Debug.Assert(source is IValueTaskSource); + IValueTaskSource typedSource = Unsafe.As(ref source); + typedSource.OnCompleted(continuation, state, token, flags); + } + } + + private static void GetResult(object source, short token, ref byte result) + { + if (source is Task t) + { + TaskAwaiter.ValidateEnd(t); + } + else + { + Debug.Assert(source is IValueTaskSource); + IValueTaskSource typedSource = Unsafe.As(ref source); + typedSource.GetResult(token); + } + } + + private static void OnCompleted(object source, Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + { + if (source is Task t) + { + Debug.Assert(state is ITaskCompletionAction); + if (!t.TryAddCompletionAction(Unsafe.As(ref state))) + { + ThreadPool.UnsafeQueueUserWorkItemInternal(state, preferLocal: true); + } + } + else + { + Debug.Assert(source is IValueTaskSource); + IValueTaskSource typedSource = Unsafe.As>(ref source); + typedSource.OnCompleted(continuation, state, token, flags); + } + } + + private static void GetResult(object source, short token, ref byte result) + { + if (source is Task t) + { + TaskAwaiter.ValidateEnd(t); + Unsafe.As(ref result) = t.ResultOnSuccess; + } + else + { + Debug.Assert(source is IValueTaskSource); + IValueTaskSource typedSource = Unsafe.As>(ref source); + Unsafe.As(ref result) = typedSource.GetResult(token); + } + } + + private static class ValueTaskContinuationResume + { + [FixedAddressValueType] + public static ResumeInfo ResumeInfo = new ResumeInfo + { + DiagnosticIP = null, + Resume = &ResumeValueTaskContinuation, + }; + + public static Continuation? ResumeValueTaskContinuation(Continuation cont, ref byte result) + { + var vtsCont = (ValueTaskContinuation)cont; + t_runtimeAsyncAwaitState.CachedValueTaskContinuation = vtsCont; + + vtsCont.GetResult(ref result); + return null; + } + } + } + + [BypassReadyToRun] + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.Async)] + private static unsafe void TransparentAwaitValueTask(ValueTask valueTask) + { + ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState; + Continuation? sentinelContinuation = state.SentinelContinuation ??= new Continuation(); + + ValueTaskContinuation? vtsCont = state.CachedValueTaskContinuation; + if (vtsCont != null) + { + state.CachedValueTaskContinuation = null; + } + else + { + vtsCont = new ValueTaskContinuation(); + } + + Debug.Assert(valueTask._obj != null); + vtsCont.Initialize(valueTask._obj, valueTask._token); + vtsCont.ExecutionContext = ExecutionContext.CaptureForSuspension(state.CurrentThread!); + + sentinelContinuation.Next = vtsCont; + state.StackState->ValueTaskContinuation = vtsCont; + + state.CaptureContexts(); + AsyncSuspend(vtsCont); + } + + [BypassReadyToRun] + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.Async)] + private static unsafe void TransparentAwaitValueTaskOfT(ValueTask valueTask) + { + ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState; + Continuation? sentinelContinuation = state.SentinelContinuation ??= new Continuation(); + + ValueTaskContinuation? vtsCont = state.CachedValueTaskContinuation; + if (vtsCont != null) + { + state.CachedValueTaskContinuation = null; + } + else + { + vtsCont = new ValueTaskContinuation(); + } + + Debug.Assert(valueTask._obj != null); + vtsCont.Initialize(valueTask._obj, valueTask._token); + vtsCont.ExecutionContext = ExecutionContext.CaptureForSuspension(state.CurrentThread!); + + sentinelContinuation.Next = vtsCont; + state.StackState->ValueTaskContinuation = vtsCont; + + state.CaptureContexts(); + AsyncSuspend(vtsCont); + } + /// /// Used by internal thunks that implement awaiting on Task or a ValueTask. /// A ValueTask may wrap: @@ -353,7 +558,7 @@ private static unsafe void TransparentAwait(object o) } else { - state.StackState->ValueTaskSourceNotifier = (ValueTaskSourceNotifier)o; + Debug.Fail("Unexpected"); } state.CaptureContexts(); @@ -456,8 +661,9 @@ internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state) ThreadPool.UnsafeQueueUserWorkItemInternal(this, preferLocal: true); } } - else if (stackState->ValueTaskSourceNotifier is { } valueTaskSourceNotifier) + else if (stackState->ValueTaskContinuation is { } valueTaskSourceCont) { + Debug.Assert(headContinuation == valueTaskSourceCont); // The awaiter must inform the ValueTaskSource on whether the continuation // wants to run on a context, although the source may decide to ignore the suggestion. // Since the behavior of the source takes precedence, we clear the context flags of @@ -491,7 +697,7 @@ internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state) // Clear continuation flags, so that continuation runs transparently nextUserContinuation.Flags &= ~continueFlags; - valueTaskSourceNotifier.OnCompleted(s_runContinuationAction, this, configFlags); + valueTaskSourceCont.OnCompleted(s_runContinuationAction, this, configFlags); } else { diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index bce89a76385ff1..5cccccd4592ccf 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -7165,7 +7165,7 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::IsValidObject(CORDB_ADDRESS obj, if (mt == cls->GetMethodTable()) isValid = TRUE; - else if (!mt->IsCanonicalMethodTable() || mt->IsContinuation()) + else if (!mt->IsCanonicalMethodTable() || (mt->IsContinuation() && !mt->IsContinuationWithMetadata())) isValid = cls->GetMethodTable()->GetClass() == cls; } EX_CATCH diff --git a/src/coreclr/inc/dacvars.h b/src/coreclr/inc/dacvars.h index 0b07ee0ceb0b15..5e32a086c7c61f 100644 --- a/src/coreclr/inc/dacvars.h +++ b/src/coreclr/inc/dacvars.h @@ -180,6 +180,7 @@ DEFINE_DACVAR(UNKNOWN_POINTER_TYPE, dac__g_pWeakReferenceClass, ::g_pWeakReferen DEFINE_DACVAR(UNKNOWN_POINTER_TYPE, dac__g_pWeakReferenceOfTClass, ::g_pWeakReferenceOfTClass) DEFINE_DACVAR_VOLATILE(UNKNOWN_POINTER_TYPE, dac__g_pContinuationClassIfSubTypeCreated, ::g_pContinuationClassIfSubTypeCreated) +DEFINE_DACVAR_VOLATILE(UNKNOWN_POINTER_TYPE, dac__g_singletonContinuationEEClass, ::g_singletonContinuationEEClass) #ifdef FEATURE_COMINTEROP DEFINE_DACVAR(UNKNOWN_POINTER_TYPE, dac__g_pBaseCOMObject, ::g_pBaseCOMObject) diff --git a/src/coreclr/tools/Common/TypeSystem/IL/Stubs/AsyncThunks.cs b/src/coreclr/tools/Common/TypeSystem/IL/Stubs/AsyncThunks.cs index d5f367c6498b0b..8dac68d3d4bb6a 100644 --- a/src/coreclr/tools/Common/TypeSystem/IL/Stubs/AsyncThunks.cs +++ b/src/coreclr/tools/Common/TypeSystem/IL/Stubs/AsyncThunks.cs @@ -292,21 +292,26 @@ public static MethodIL EmitAsyncMethodThunk(MethodDesc asyncMethod, MethodDesc t TypeDesc valueTaskType = taskReturningMethodReturnType; MethodDesc isCompletedMethod; MethodDesc completionResultMethod; - MethodDesc asTaskOrNotifierMethod; + MethodDesc transparentAwaitValueTaskMethod; if (!taskReturningMethodReturnType.HasInstantiation) { // ValueTask (non-generic) isCompletedMethod = valueTaskType.GetKnownMethod("get_IsCompleted"u8, null); completionResultMethod = valueTaskType.GetKnownMethod("ThrowIfCompletedUnsuccessfully"u8, null); - asTaskOrNotifierMethod = valueTaskType.GetKnownMethod("AsTaskOrNotifier"u8, null); + transparentAwaitValueTaskMethod = + context.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8) + .GetKnownMethod("TransparentAwaitValueTask"u8, null); } else { // ValueTask (generic) isCompletedMethod = valueTaskType.GetKnownMethod("get_IsCompleted"u8, null); completionResultMethod = valueTaskType.GetKnownMethod("get_Result"u8, null); - asTaskOrNotifierMethod = valueTaskType.GetKnownMethod("AsTaskOrNotifier"u8, null); + transparentAwaitValueTaskMethod = + context.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8) + .GetKnownMethod("TransparentAwaitValueTaskOfT"u8, null) + .MakeInstantiatedMethod(valueTaskType.Instantiation[0]); } ILLocalVariable valueTaskLocal = emitter.NewLocal(valueTaskType); @@ -315,15 +320,17 @@ public static MethodIL EmitAsyncMethodThunk(MethodDesc asyncMethod, MethodDesc t // Store value task returned by call to actual user func codestream.EmitStLoc(valueTaskLocal); codestream.EmitLdLoca(valueTaskLocal); + + // Was it already completed? codestream.Emit(ILOpcode.call, emitter.NewToken(isCompletedMethod)); codestream.Emit(ILOpcode.brtrue, valueTaskCompletedLabel); - codestream.EmitLdLoca(valueTaskLocal); - codestream.Emit(ILOpcode.call, emitter.NewToken(asTaskOrNotifierMethod)); - codestream.Emit(ILOpcode.call, emitter.NewToken( - context.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8) - .GetKnownMethod("TransparentAwait"u8, null))); + // No, tail await to TransparentAwaitValueTask + codestream.EmitLdLoc(valueTaskLocal); + codestream.Emit(ILOpcode.call, emitter.NewToken(context.GetCoreLibEntryPoint("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8, "TailAwait"u8, null))); + codestream.Emit(ILOpcode.call, emitter.NewToken(transparentAwaitValueTaskMethod)); + // Yes, just get the result codestream.EmitLabel(valueTaskCompletedLabel); codestream.EmitLdLoca(valueTaskLocal); codestream.Emit(ILOpcode.call, emitter.NewToken(completionResultMethod)); diff --git a/src/coreclr/vm/asynccontinuations.cpp b/src/coreclr/vm/asynccontinuations.cpp index 0005682e3670ac..b139fd45a61a36 100644 --- a/src/coreclr/vm/asynccontinuations.cpp +++ b/src/coreclr/vm/asynccontinuations.cpp @@ -43,8 +43,6 @@ void AsyncContinuationsManager::NotifyUnloadingClasses() #endif // PROFILING_SUPPORTED } -static EEClass* volatile g_singletonContinuationEEClass; - EEClass* AsyncContinuationsManager::GetOrCreateSingletonSubContinuationEEClass() { if (g_singletonContinuationEEClass != NULL) diff --git a/src/coreclr/vm/asyncthunks.cpp b/src/coreclr/vm/asyncthunks.cpp index ef45a4a41c715d..c3dfa0a60fc84a 100644 --- a/src/coreclr/vm/asyncthunks.cpp +++ b/src/coreclr/vm/asyncthunks.cpp @@ -499,10 +499,8 @@ void MethodDesc::EmitAsyncMethodThunk(MethodDesc* pTaskReturningVariant, MetaSig // ValueTask vt = other(arg); // if (!vt.IsCompleted) // { - // taskOrNotifier = vt.AsTaskOrNotifier() - - // // Magic function which will suspend the current run of async methods - // AsyncHelpers.TransparentAwait(taskOrNotifier); + // TailAwait(); + // AsyncHelpers.TransparentAwaitValueTask(vt); // } // return vt.Result/vt.ThrowIfCompletedUnsuccessfully(); @@ -538,18 +536,18 @@ void MethodDesc::EmitAsyncMethodThunk(MethodDesc* pTaskReturningVariant, MetaSig MethodTable* pMTValueTask; int isCompletedToken; int completionResultToken; - int asTaskOrNotifierToken; + int transparentAwaitValueTaskToken; if (msig.IsReturnTypeVoid()) { pMTValueTask = CoreLibBinder::GetClass(CLASS__VALUETASK); MethodDesc* pMDValueTaskIsCompleted = CoreLibBinder::GetMethod(METHOD__VALUETASK__GET_ISCOMPLETED); MethodDesc* pMDCompletionResult = CoreLibBinder::GetMethod(METHOD__VALUETASK__THROW_IF_COMPLETED_UNSUCCESSFULLY); - MethodDesc* pMDAsTaskOrNotifier = CoreLibBinder::GetMethod(METHOD__VALUETASK__AS_TASK_OR_NOTIFIER); + MethodDesc* pMDTransparentAwaitValueTask = CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__TRANSPARENT_AWAIT_VALUE_TASK); isCompletedToken = pCode->GetToken(pMDValueTaskIsCompleted); completionResultToken = pCode->GetToken(pMDCompletionResult); - asTaskOrNotifierToken = pCode->GetToken(pMDAsTaskOrNotifier); + transparentAwaitValueTaskToken = pCode->GetToken(pMDTransparentAwaitValueTask); } else { @@ -558,15 +556,15 @@ void MethodDesc::EmitAsyncMethodThunk(MethodDesc* pTaskReturningVariant, MetaSig MethodDesc* pMDValueTaskIsCompleted = CoreLibBinder::GetMethod(METHOD__VALUETASK_1__GET_ISCOMPLETED); MethodDesc* pMDCompletionResult = CoreLibBinder::GetMethod(METHOD__VALUETASK_1__GET_RESULT); - MethodDesc* pMDAsTaskOrNotifier = CoreLibBinder::GetMethod(METHOD__VALUETASK_1__AS_TASK_OR_NOTIFIER); + MethodDesc* pMDTransparentAwaitValueTask = CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__TRANSPARENT_AWAIT_VALUE_TASK_OF_T); pMDValueTaskIsCompleted = FindOrCreateAssociatedMethodDesc(pMDValueTaskIsCompleted, pMTValueTask, FALSE, Instantiation(), FALSE); pMDCompletionResult = FindOrCreateAssociatedMethodDesc(pMDCompletionResult, pMTValueTask, FALSE, Instantiation(), FALSE); - pMDAsTaskOrNotifier = FindOrCreateAssociatedMethodDesc(pMDAsTaskOrNotifier, pMTValueTask, FALSE, Instantiation(), FALSE); + pMDTransparentAwaitValueTask = FindOrCreateAssociatedMethodDesc(pMDTransparentAwaitValueTask, pMDTransparentAwaitValueTask->GetMethodTable(), FALSE, Instantiation(&thLogicalRetType, 1), FALSE); isCompletedToken = GetTokenForGenericTypeMethodCallWithAsyncReturnType(pCode, pMDValueTaskIsCompleted); completionResultToken = GetTokenForGenericTypeMethodCallWithAsyncReturnType(pCode, pMDCompletionResult); - asTaskOrNotifierToken = GetTokenForGenericTypeMethodCallWithAsyncReturnType(pCode, pMDAsTaskOrNotifier); + transparentAwaitValueTaskToken = GetTokenForGenericMethodCallWithAsyncReturnType(pCode, pMDTransparentAwaitValueTask); } LocalDesc valueTaskLocalDesc(pMTValueTask); @@ -576,13 +574,17 @@ void MethodDesc::EmitAsyncMethodThunk(MethodDesc* pTaskReturningVariant, MetaSig // Store value task returned by call to actual user func pCode->EmitSTLOC(valueTaskLocal); pCode->EmitLDLOCA(valueTaskLocal); + + // Was it already completed? pCode->EmitCALL(isCompletedToken, 1, 1); pCode->EmitBRTRUE(valueTaskCompletedLabel); - pCode->EmitLDLOCA(valueTaskLocal); - pCode->EmitCALL(asTaskOrNotifierToken, 1, 1); - pCode->EmitCALL(METHOD__ASYNC_HELPERS__TRANSPARENT_AWAIT, 1, 0); + // No, tail await to TransparentAwaitValueTask + pCode->EmitLDLOC(valueTaskLocal); + pCode->EmitCALL(METHOD__ASYNC_HELPERS__TAIL_AWAIT, 0, 0); + pCode->EmitCALL(transparentAwaitValueTaskToken, 1, 0); + // Yes, just get the result pCode->EmitLabel(valueTaskCompletedLabel); pCode->EmitLDLOCA(valueTaskLocal); pCode->EmitCALL(completionResultToken, 1, msig.IsReturnTypeVoid() ? 0 : 1); diff --git a/src/coreclr/vm/corelib.h b/src/coreclr/vm/corelib.h index 485792072ae4d5..4a36a6518a0446 100644 --- a/src/coreclr/vm/corelib.h +++ b/src/coreclr/vm/corelib.h @@ -328,14 +328,12 @@ DEFINE_METHOD(FILE_LOAD_EXCEPTION, CREATE, Create, NoSig) DEFINE_CLASS(VALUETASK_1, Tasks, ValueTask`1) DEFINE_METHOD(VALUETASK_1, GET_ISCOMPLETED, get_IsCompleted, NoSig) DEFINE_METHOD(VALUETASK_1, GET_RESULT, get_Result, NoSig) -DEFINE_METHOD(VALUETASK_1, AS_TASK_OR_NOTIFIER, AsTaskOrNotifier, IM_RetObj) DEFINE_CLASS(VALUETASK, Tasks, ValueTask) DEFINE_METHOD(VALUETASK, FROM_RESULT_T, FromResult, GM_T_RetValueTaskOfT) DEFINE_METHOD(VALUETASK, GET_COMPLETED_TASK, get_CompletedTask, SM_RetValueTask) DEFINE_METHOD(VALUETASK, GET_ISCOMPLETED, get_IsCompleted, NoSig) DEFINE_METHOD(VALUETASK, THROW_IF_COMPLETED_UNSUCCESSFULLY, ThrowIfCompletedUnsuccessfully, NoSig) -DEFINE_METHOD(VALUETASK, AS_TASK_OR_NOTIFIER, AsTaskOrNotifier, IM_RetObj) DEFINE_CLASS(TASK_1, Tasks, Task`1) @@ -717,7 +715,9 @@ DEFINE_METHOD(ASYNC_HELPERS, TASK_FROM_EXCEPTION_1, TaskFromException, GM_E DEFINE_METHOD(ASYNC_HELPERS, VALUETASK_FROM_EXCEPTION, ValueTaskFromException, SM_Exception_RetValueTask) DEFINE_METHOD(ASYNC_HELPERS, VALUETASK_FROM_EXCEPTION_1, ValueTaskFromException, GM_Exception_RetValueTaskOfT) -DEFINE_METHOD(ASYNC_HELPERS, TRANSPARENT_AWAIT, TransparentAwait, NoSig) +DEFINE_METHOD(ASYNC_HELPERS, TRANSPARENT_AWAIT, TransparentAwait, NoSig) +DEFINE_METHOD(ASYNC_HELPERS, TRANSPARENT_AWAIT_VALUE_TASK, TransparentAwaitValueTask, NoSig) +DEFINE_METHOD(ASYNC_HELPERS, TRANSPARENT_AWAIT_VALUE_TASK_OF_T, TransparentAwaitValueTaskOfT, NoSig) DEFINE_METHOD(ASYNC_HELPERS, COMPLETED_TASK_RESULT, CompletedTaskResult, NoSig) DEFINE_METHOD(ASYNC_HELPERS, COMPLETED_TASK, CompletedTask, NoSig) DEFINE_METHOD(ASYNC_HELPERS, CAPTURE_EXECUTION_CONTEXT, CaptureExecutionContext, NoSig) @@ -729,6 +729,7 @@ DEFINE_METHOD(ASYNC_HELPERS, FINISH_SUSPENSION_NO_CONTINUATION_CONTEXT, Fin DEFINE_METHOD(ASYNC_HELPERS, FINISH_SUSPENSION_WITH_CONTINUATION_CONTEXT, FinishSuspensionWithContinuationContext, NoSig) DEFINE_METHOD(ASYNC_HELPERS, ASYNC_CALL_CONTINUATION, AsyncCallContinuation, NoSig) DEFINE_METHOD(ASYNC_HELPERS, TAIL_AWAIT, TailAwait, NoSig) + DEFINE_FIELD(ASYNC_HELPERS, TLS_RUNTIME_ASYNC_AWAIT_STATE, t_runtimeAsyncAwaitState) #ifdef FEATURE_INTERPRETER diff --git a/src/coreclr/vm/methodtable.cpp b/src/coreclr/vm/methodtable.cpp index 5a5a714a360edf..e7f6e9205d0f18 100644 --- a/src/coreclr/vm/methodtable.cpp +++ b/src/coreclr/vm/methodtable.cpp @@ -421,7 +421,7 @@ WORD MethodTable::GetNumMethods() PTR_MethodTable MethodTable::GetTypicalMethodTable() { LIMITED_METHOD_DAC_CONTRACT; - if (IsArray() || IsContinuation()) + if (IsArray() || (IsContinuation() && !IsContinuationWithMetadata())) return (PTR_MethodTable)this; PTR_MethodTable methodTableMaybe = GetModule()->LookupTypeDef(GetCl()).AsMethodTable(); @@ -1297,7 +1297,7 @@ BOOL MethodTable::CanCastToClass(MethodTable *pTargetMT, TypeHandlePairList *pVi PRECONDITION(CheckPointer(pTargetMT)); PRECONDITION(!pTargetMT->IsArray()); PRECONDITION(!pTargetMT->IsInterface()); - PRECONDITION(!pTargetMT->IsContinuation()); + PRECONDITION(!pTargetMT->IsContinuation() || pTargetMT->IsContinuationWithMetadata()); } CONTRACTL_END @@ -6399,6 +6399,11 @@ BOOL MethodTable::SanityCheck() return (pCanonMT == this) || IsArray() || IsContinuation(); } +BOOL MethodTable::IsContinuationWithMetadata() +{ + LIMITED_METHOD_DAC_CONTRACT; + return IsContinuation() && (GetClass() != g_singletonContinuationEEClass); +} //========================================================================================== void MethodTable::SetCl(mdTypeDef token) @@ -7642,7 +7647,7 @@ CHECK MethodTable::CheckInstanceActivated() { WRAPPER_NO_CONTRACT; - if (IsArray() || IsContinuation()) + if (IsArray() || (IsContinuation() && !IsContinuationWithMetadata())) CHECK_OK; diff --git a/src/coreclr/vm/methodtable.h b/src/coreclr/vm/methodtable.h index 402b0cde9b15bd..a894828b5564fd 100644 --- a/src/coreclr/vm/methodtable.h +++ b/src/coreclr/vm/methodtable.h @@ -985,7 +985,7 @@ class MethodTable PTR_Module GetModule() { LIMITED_METHOD_CONTRACT; - _ASSERTE(!IsContinuation()); + _ASSERTE(!IsContinuation() || IsContinuationWithMetadata()); return m_pModule; } @@ -2853,6 +2853,7 @@ class MethodTable } inline BOOL IsContinuation(); + inline BOOL IsContinuationWithMetadata(); // The following methods are only valid for the method tables for array types. CorElementType GetArrayElementType() diff --git a/src/coreclr/vm/typehandle.cpp b/src/coreclr/vm/typehandle.cpp index 5dd7d4e85d468a..c7861807aec6c0 100644 --- a/src/coreclr/vm/typehandle.cpp +++ b/src/coreclr/vm/typehandle.cpp @@ -98,6 +98,13 @@ BOOL TypeHandle::IsContinuation() const return !IsTypeDesc() && AsMethodTable()->IsContinuation(); } +BOOL TypeHandle::IsContinuationWithMetadata() const +{ + LIMITED_METHOD_CONTRACT; + + return !IsTypeDesc() && AsMethodTable()->IsContinuationWithMetadata(); +} + BOOL TypeHandle::IsGenericVariable() const { LIMITED_METHOD_DAC_CONTRACT; @@ -333,7 +340,7 @@ void TypeHandle::AllocateManagedClassObject(RUNTIMETYPEHANDLE* pDest) } CONTRACTL_END - if (IsContinuation()) + if (IsContinuation() && !IsContinuationWithMetadata()) { COMPlusThrow(kNotSupportedException, W("NotSupported_Continuation")); return; diff --git a/src/coreclr/vm/typehandle.h b/src/coreclr/vm/typehandle.h index 1fb2f944bb509d..d1734779b0b306 100644 --- a/src/coreclr/vm/typehandle.h +++ b/src/coreclr/vm/typehandle.h @@ -444,6 +444,7 @@ class TypeHandle // Continuation sub types BOOL IsContinuation() const; + BOOL IsContinuationWithMetadata() const; // True if this type *is* a formal generic type parameter or any component of it is a formal generic type parameter BOOL ContainsGenericVariables(BOOL methodOnly=FALSE) const; diff --git a/src/coreclr/vm/typehandle.inl b/src/coreclr/vm/typehandle.inl index d1916c121e56c2..99edadbcb2fc46 100644 --- a/src/coreclr/vm/typehandle.inl +++ b/src/coreclr/vm/typehandle.inl @@ -38,7 +38,7 @@ inline TypeHandle TypeHandle::UpCastTypeIfNeeded() const if (IsTypeDesc()) return *this; - if (AsMethodTable()->IsContinuation()) + if (AsMethodTable()->IsContinuation() && !AsMethodTable()->IsContinuationWithMetadata()) { return TypeHandle(g_pContinuationClassIfSubTypeCreated); } diff --git a/src/coreclr/vm/vars.cpp b/src/coreclr/vm/vars.cpp index a567a206749c62..8567eabc0c1d1c 100644 --- a/src/coreclr/vm/vars.cpp +++ b/src/coreclr/vm/vars.cpp @@ -68,8 +68,10 @@ GPTR_IMPL(MethodTable, g_pWeakReferenceOfTClass); #ifdef DACCESS_COMPILE GPTR_IMPL(MethodTable, g_pContinuationClassIfSubTypeCreated); +GPTR_IMPL(EEClass, g_singletonContinuationEEClass); #else GVAL_IMPL(Volatile, g_pContinuationClassIfSubTypeCreated); +GVAL_IMPL(Volatile, g_singletonContinuationEEClass); #endif #ifdef FEATURE_COMINTEROP diff --git a/src/coreclr/vm/vars.hpp b/src/coreclr/vm/vars.hpp index acae3ae57e7817..73df857cee3d40 100644 --- a/src/coreclr/vm/vars.hpp +++ b/src/coreclr/vm/vars.hpp @@ -359,8 +359,10 @@ GPTR_DECL(MethodTable, g_pWeakReferenceOfTClass); #ifdef DACCESS_COMPILE GPTR_DECL(MethodTable, g_pContinuationClassIfSubTypeCreated); +GPTR_DECL(EEClass, g_singletonContinuationEEClass); #else GVAL_DECL(Volatile, g_pContinuationClassIfSubTypeCreated); +GVAL_DECL(Volatile, g_singletonContinuationEEClass); #endif #ifdef FEATURE_COMINTEROP diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs index 4eaf2679966461..90f86c68159c28 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs @@ -179,21 +179,6 @@ obj as Task ?? GetTaskForValueTaskSource(Unsafe.As(obj)); } - /// - /// Helper to invoke IValueTaskSource.OnCompleted from a caller that does not know the actual type of the source. - /// - private static void OnCompleted(object obj, Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => - Unsafe.As(obj).OnCompleted(continuation, state, token, flags); - - internal unsafe object AsTaskOrNotifier() - { - object? obj = _obj; - Debug.Assert(obj is Task || obj is IValueTaskSource); - return - obj as Task ?? - (object)ValueTaskSourceNotifier.GetInstance(obj, &OnCompleted, _token); - } - /// Gets a that may be used at any point in the future. public ValueTask Preserve() => _obj == null ? this : new ValueTask(AsTask()); @@ -603,21 +588,6 @@ public Task AsTask() return GetTaskForValueTaskSource(Unsafe.As>(obj)); } - /// - /// Helper to invoke IValueTaskSource.OnCompleted from a caller that does not know the actual type of the source. - /// - private static void OnCompleted(object obj, Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => - Unsafe.As>(obj).OnCompleted(continuation, state, token, flags); - - internal unsafe object AsTaskOrNotifier() - { - object? obj = _obj; - Debug.Assert(obj is Task || obj is IValueTaskSource); - return - obj as Task ?? - (object)ValueTaskSourceNotifier.GetInstance(obj, &OnCompleted, _token); - } - /// Gets a that may be used at any point in the future. public ValueTask Preserve() => _obj == null ? this : new ValueTask(AsTask()); @@ -878,54 +848,4 @@ public ConfiguredValueTaskAwaitable ConfigureAwait(bool continueOnCaptu return string.Empty; } } - - internal sealed unsafe class ValueTaskSourceNotifier - { - // ValueTaskSourceNotifier is used only during suspension sequence, thus - // a given thread will never need more than one instance. - // We will just reuse the same instance when needed. - [ThreadStatic] - private static ValueTaskSourceNotifier? t_instance; - - private object _source; - private delegate* managed, object?, short, ValueTaskSourceOnCompletedFlags, void> _onCompleted; - private short _token; - - private ValueTaskSourceNotifier( - object source, - delegate* managed, object?, short, ValueTaskSourceOnCompletedFlags, void> onCompleted, - short token) - { - _source = source; - _onCompleted = onCompleted; - _token = token; - } - - public static ValueTaskSourceNotifier GetInstance( - object source, - delegate* managed, object?, short, ValueTaskSourceOnCompletedFlags, void> onCompleted, - short token) - { - ValueTaskSourceNotifier? instance = t_instance; - if (instance == null) - { - return t_instance = new ValueTaskSourceNotifier(source, onCompleted, token); - } - - instance._source = source; - instance._onCompleted = onCompleted; - instance._token = token; - - return instance; - } - - public void OnCompleted(Action continuation, object? state, ValueTaskSourceOnCompletedFlags flags) - { - _onCompleted(_source, continuation, state, _token, flags); - - // The data that we store is effectively single-use. - // Once used, clear the _source to not retain unknown data. - _source = null!; - } - } } From b262711c818936c0400e81692069c6798dd19a18 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Sat, 9 May 2026 00:04:34 +0200 Subject: [PATCH 2/2] Feedback --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 15 +++++---------- src/coreclr/vm/methodtable.h | 2 +- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs index d66a34d883a59a..cf49a8b3b54dca 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs @@ -471,6 +471,8 @@ private static class ValueTaskContinuationResume public static Continuation? ResumeValueTaskContinuation(Continuation cont, ref byte result) { var vtsCont = (ValueTaskContinuation)cont; + vtsCont.Next = null; + vtsCont.ExecutionContext = null; t_runtimeAsyncAwaitState.CachedValueTaskContinuation = vtsCont; vtsCont.GetResult(ref result); @@ -544,22 +546,15 @@ private static unsafe void TransparentAwaitValueTaskOfT(ValueTask valueTa /// Therefore, when we are awaiting a ValueTask completion we are really /// awaiting a completion of an underlying Task or ValueTaskSource. /// - /// Task or a ValueTaskNotifier whose completion we are awaiting. + /// Task whose completion we are awaiting. [BypassReadyToRun] [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.Async)] - private static unsafe void TransparentAwait(object o) + private static unsafe void TransparentAwait(Task t) { ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState; Continuation? sentinelContinuation = state.SentinelContinuation ??= new Continuation(); - if (o is Task t) - { - state.StackState->TaskNotifier = t; - } - else - { - Debug.Fail("Unexpected"); - } + state.StackState->TaskNotifier = t; state.CaptureContexts(); AsyncSuspend(sentinelContinuation); diff --git a/src/coreclr/vm/methodtable.h b/src/coreclr/vm/methodtable.h index a894828b5564fd..392dc2165c43d4 100644 --- a/src/coreclr/vm/methodtable.h +++ b/src/coreclr/vm/methodtable.h @@ -2853,7 +2853,7 @@ class MethodTable } inline BOOL IsContinuation(); - inline BOOL IsContinuationWithMetadata(); + BOOL IsContinuationWithMetadata(); // The following methods are only valid for the method tables for array types. CorElementType GetArrayElementType()