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 a7ab5415d25d97..2fde8dac73caa9 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 @@ -10,6 +10,7 @@ using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; +using System.Threading.Tasks.Sources; namespace System.Runtime.CompilerServices { @@ -491,6 +492,50 @@ public static void HandleSuspended(T task) where T : Task, ITaskComplet } else if (calledTask != null) { + if (calledTask is IValueTaskAsTask vtTask) + { + if (!vtTask.IsConfigured) + { + // ValueTaskSource can be configured to use scheduling context or to ignore it. + // The awaiter must inform the source whether it wants to continue on a context, + // but the source may decide to ignore the suggestion. Since the behavior of + // the source takes precedence, we clear the context flags from the awaiting + // continuation (so it will run transparently on what source decides) and tell + // the source that awaiting frame prefers to continue on a context. + // The reason why we do it here is because the continuation chain builds from the + // innermost frame out and when the leaf thunk links the head continuation, + // it does not know if the caller wants to continue in a context. Thus the thunk + // creates an "unconfigured" IValueTaskAsTask and we configure it here. + ValueTaskSourceOnCompletedFlags configFlags = ValueTaskSourceOnCompletedFlags.None; + CorInfoContinuationFlags continuationFlags = headContinuation.Next!.Flags; + + const CorInfoContinuationFlags continueOnContextFlags = + CorInfoContinuationFlags.CORINFO_CONTINUATION_CONTINUE_ON_CAPTURED_SYNCHRONIZATION_CONTEXT | + CorInfoContinuationFlags.CORINFO_CONTINUATION_CONTINUE_ON_CAPTURED_TASK_SCHEDULER; + + if ((continuationFlags & continueOnContextFlags) != 0) + { + // if await has captured some context, inform the source + configFlags |= ValueTaskSourceOnCompletedFlags.UseSchedulingContext; + } + + // clear continuation flags, so that continuation runs transparently + headContinuation.Next!.Flags &= ~continueFlags; + vtTask.Configure(configFlags); + + if (!calledTask.TryAddCompletionAction(task)) + { + // calledTask has already completed and we need to schedule + // the continuation for execution ourselves. + // Restore the continuation flags before doing that. + headContinuation.Next!.Flags = continuationFlags; + ThreadPool.UnsafeQueueUserWorkItemInternal(task, preferLocal: true); + } + + return; + } + } + // Runtime async callable wrapper for task returning // method. This implements the context transparent // forwarding and makes these wrappers minimal cost. diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index 542510f79b4a74..3c2a9c3dab3dd8 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -720,7 +720,7 @@ RETAIL_CONFIG_DWORD_INFO(EXTERNAL_EnableRiscV64Zbb, W("EnableRiscV64 #endif // Runtime-async -RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_RuntimeAsync, W("RuntimeAsync"), 0, "Enables runtime async method support") +RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_RuntimeAsync, W("RuntimeAsync"), 1, "Enables runtime async method support") /// /// Uncategorized diff --git a/src/coreclr/vm/asyncthunks.cpp b/src/coreclr/vm/asyncthunks.cpp index cf4e290bc6e7c4..ccc0c615d23d8f 100644 --- a/src/coreclr/vm/asyncthunks.cpp +++ b/src/coreclr/vm/asyncthunks.cpp @@ -591,7 +591,7 @@ void MethodDesc::EmitAsyncMethodThunk(MethodDesc* pAsyncOtherVariant, MetaSig& m MethodDesc* pMDValueTaskIsCompleted = CoreLibBinder::GetMethod(METHOD__VALUETASK__GET_ISCOMPLETED); MethodDesc* pMDCompletionResult = CoreLibBinder::GetMethod(METHOD__VALUETASK__THROW_IF_COMPLETED_UNSUCCESSFULLY); - MethodDesc* pMDAsTask = CoreLibBinder::GetMethod(METHOD__VALUETASK__AS_TASK); + MethodDesc* pMDAsTask = CoreLibBinder::GetMethod(METHOD__VALUETASK__AS_UNCONFIGURED_TASK); isCompletedToken = pCode->GetToken(pMDValueTaskIsCompleted); completionResultToken = pCode->GetToken(pMDCompletionResult); @@ -604,7 +604,7 @@ void MethodDesc::EmitAsyncMethodThunk(MethodDesc* pAsyncOtherVariant, MetaSig& m MethodDesc* pMDValueTaskIsCompleted = CoreLibBinder::GetMethod(METHOD__VALUETASK_1__GET_ISCOMPLETED); MethodDesc* pMDCompletionResult = CoreLibBinder::GetMethod(METHOD__VALUETASK_1__GET_RESULT); - MethodDesc* pMDAsTask = CoreLibBinder::GetMethod(METHOD__VALUETASK_1__AS_TASK); + MethodDesc* pMDAsTask = CoreLibBinder::GetMethod(METHOD__VALUETASK_1__AS_UNCONFIGURED_TASK); pMDValueTaskIsCompleted = FindOrCreateAssociatedMethodDesc(pMDValueTaskIsCompleted, pMTValueTask, FALSE, Instantiation(), FALSE); pMDCompletionResult = FindOrCreateAssociatedMethodDesc(pMDCompletionResult, pMTValueTask, FALSE, Instantiation(), FALSE); diff --git a/src/coreclr/vm/corelib.h b/src/coreclr/vm/corelib.h index bfc6cc586ca38c..b9429f2591a31c 100644 --- a/src/coreclr/vm/corelib.h +++ b/src/coreclr/vm/corelib.h @@ -346,7 +346,7 @@ DEFINE_METHOD(THREAD_START_EXCEPTION,EX_CTOR, .ctor, 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, AsTask, IM_RetTaskOfT) +DEFINE_METHOD(VALUETASK_1, AS_UNCONFIGURED_TASK, AsUnconfiguredTask, IM_RetTaskOfT) DEFINE_CLASS(VALUETASK, Tasks, ValueTask) DEFINE_METHOD(VALUETASK, FROM_EXCEPTION, FromException, SM_Exception_RetValueTask) @@ -355,7 +355,7 @@ 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, AsTask, IM_RetTask) +DEFINE_METHOD(VALUETASK, AS_UNCONFIGURED_TASK, AsUnconfiguredTask, IM_RetTask) DEFINE_CLASS(TASK_1, Tasks, Task`1) DEFINE_METHOD(TASK_1, GET_RESULTONSUCCESS, get_ResultOnSuccess, NoSig) 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 90f86c68159c28..87185e18eed710 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 @@ -176,7 +176,17 @@ public Task AsTask() return obj == null ? Task.CompletedTask : obj as Task ?? - GetTaskForValueTaskSource(Unsafe.As(obj)); + GetTaskForValueTaskSource(Unsafe.As(obj), ValueTaskSourceOnCompletedFlags.None); + } + + internal Task AsUnconfiguredTask() + { + object? obj = _obj; + Debug.Assert(obj == null || obj is Task || obj is IValueTaskSource); + return + obj == null ? Task.CompletedTask : + obj as Task ?? + GetTaskForValueTaskSource(Unsafe.As(obj), IValueTaskAsTask._unconfigured); } /// Gets a that may be used at any point in the future. @@ -187,7 +197,7 @@ obj as Task ?? /// The is passed in rather than reading and casting /// so that the caller can pass in an object it's already validated. /// - private Task GetTaskForValueTaskSource(IValueTaskSource t) + private Task GetTaskForValueTaskSource(IValueTaskSource t, ValueTaskSourceOnCompletedFlags flags) { ValueTaskSourceStatus status = t.GetStatus(_token); if (status != ValueTaskSourceStatus.Pending) @@ -225,11 +235,11 @@ private Task GetTaskForValueTaskSource(IValueTaskSource t) } } - return new ValueTaskSourceAsTask(t, _token); + return new ValueTaskSourceAsTask(t, _token, flags); } /// Type used to create a to represent a . - private sealed class ValueTaskSourceAsTask : Task + private sealed class ValueTaskSourceAsTask : Task, IValueTaskAsTask { private static readonly Action s_completionAction = static state => { @@ -283,11 +293,28 @@ state is ValueTaskSourceAsTask vsts ? /// The token to pass through to operations on private readonly short _token; - internal ValueTaskSourceAsTask(IValueTaskSource source, short token) + private bool _configured; + + bool IValueTaskAsTask.IsConfigured => _configured; + void IValueTaskAsTask.Configure(ValueTaskSourceOnCompletedFlags flags) => Configure(flags); + + private void Configure(ValueTaskSourceOnCompletedFlags flags) + { + Debug.Assert(!_configured); + + _configured = true; + _source!.OnCompleted(s_completionAction, this, _token, flags); + } + + internal ValueTaskSourceAsTask(IValueTaskSource source, short token, ValueTaskSourceOnCompletedFlags flags) { _token = token; _source = source; - source.OnCompleted(s_completionAction, this, token, ValueTaskSourceOnCompletedFlags.None); + + if (flags != IValueTaskAsTask._unconfigured) + { + Configure(flags); + } } } @@ -585,7 +612,25 @@ public Task AsTask() return t; } - return GetTaskForValueTaskSource(Unsafe.As>(obj)); + return GetTaskForValueTaskSource(Unsafe.As>(obj), ValueTaskSourceOnCompletedFlags.None); + } + + internal Task AsUnconfiguredTask() + { + object? obj = _obj; + Debug.Assert(obj == null || obj is Task || obj is IValueTaskSource); + + if (obj == null) + { + return Task.FromResult(_result!); + } + + if (obj is Task t) + { + return t; + } + + return GetTaskForValueTaskSource(Unsafe.As>(obj), IValueTaskAsTask._unconfigured); } /// Gets a that may be used at any point in the future. @@ -596,7 +641,7 @@ public Task AsTask() /// The is passed in rather than reading and casting /// so that the caller can pass in an object it's already validated. /// - private Task GetTaskForValueTaskSource(IValueTaskSource t) + private Task GetTaskForValueTaskSource(IValueTaskSource t, ValueTaskSourceOnCompletedFlags flags) { ValueTaskSourceStatus status = t.GetStatus(_token); if (status != ValueTaskSourceStatus.Pending) @@ -633,11 +678,11 @@ private Task GetTaskForValueTaskSource(IValueTaskSource t) } } - return new ValueTaskSourceAsTask(t, _token); + return new ValueTaskSourceAsTask(t, _token, flags); } /// Type used to create a to represent a . - private sealed class ValueTaskSourceAsTask : Task + private sealed class ValueTaskSourceAsTask : Task, IValueTaskAsTask { private static readonly Action s_completionAction = static state => { @@ -690,11 +735,28 @@ state is ValueTaskSourceAsTask vsts ? /// The token to pass through to operations on private readonly short _token; - public ValueTaskSourceAsTask(IValueTaskSource source, short token) + private bool _configured; + + bool IValueTaskAsTask.IsConfigured => _configured; + void IValueTaskAsTask.Configure(ValueTaskSourceOnCompletedFlags flags) => Configure(flags); + + private void Configure(ValueTaskSourceOnCompletedFlags flags) + { + Debug.Assert(!_configured); + + _configured = true; + _source!.OnCompleted(s_completionAction, this, _token, flags); + } + + internal ValueTaskSourceAsTask(IValueTaskSource source, short token, ValueTaskSourceOnCompletedFlags flags) { - _source = source; _token = token; - source.OnCompleted(s_completionAction, this, token, ValueTaskSourceOnCompletedFlags.None); + _source = source; + + if (flags != IValueTaskAsTask._unconfigured) + { + Configure(flags); + } } } @@ -848,4 +910,12 @@ public ConfiguredValueTaskAwaitable ConfigureAwait(bool continueOnCaptu return string.Empty; } } + + internal interface IValueTaskAsTask + { + internal const ValueTaskSourceOnCompletedFlags _unconfigured = (ValueTaskSourceOnCompletedFlags)(-1); + + bool IsConfigured { get; } + void Configure(ValueTaskSourceOnCompletedFlags flags); + } } diff --git a/src/tests/async/Directory.Build.props b/src/tests/async/Directory.Build.props index a1f7d48cf60a7d..f505092384b1b1 100644 --- a/src/tests/async/Directory.Build.props +++ b/src/tests/async/Directory.Build.props @@ -5,7 +5,7 @@ true - $(NoWarn);xUnit1013;CS1998 + $(NoWarn);xUnit1013;CS1998;SYSLIB5007 false $(Features);runtime-async=on diff --git a/src/tests/async/Directory.Build.targets b/src/tests/async/Directory.Build.targets index 5a4d413a9e1f62..61a1b2822ed114 100644 --- a/src/tests/async/Directory.Build.targets +++ b/src/tests/async/Directory.Build.targets @@ -4,10 +4,5 @@ true - - - true - - diff --git a/src/tests/async/struct/struct.cs b/src/tests/async/struct/struct.cs index 7c5062d499cb42..32116fe3cec777 100644 --- a/src/tests/async/struct/struct.cs +++ b/src/tests/async/struct/struct.cs @@ -45,6 +45,8 @@ private struct S public S(int value) => Value = value; + // Roslyn NYI - async in structs. Remove opt-out once supported. + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)] public async Task Test() { // TODO: C# compiler is expected to do this, but not in the prototype. @@ -59,6 +61,8 @@ public async Task Test() AssertEqual(102, @this.Value); } + // Roslyn NYI - async in structs. Remove opt-out once supported. + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)] private async Task InstanceCall() { // TODO: C# compiler is expected to do this, but not in the prototype. diff --git a/src/tests/async/valuetask Source/valuetaskSource.cs b/src/tests/async/valuetask Source/valuetaskSource.cs new file mode 100644 index 00000000000000..3bf080a1f93a09 --- /dev/null +++ b/src/tests/async/valuetask Source/valuetaskSource.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; +using Xunit; + +public class Async2ValueTaskSource +{ + static string trace; + + [Fact] + static void TestEntry() + { + ValueTask vtUseContext = new ValueTask(new Source(true), 1); + + AwaitConfigDefault(vtUseContext).GetAwaiter().GetResult(); + Assert.Equal("UseSchedulingContext", trace); + AwaitConfigTrue(vtUseContext).GetAwaiter().GetResult(); + Assert.Equal("UseSchedulingContext", trace); + AwaitConfigFalse(vtUseContext).GetAwaiter().GetResult(); + Assert.Equal("None", trace); + + ValueTask vtIgnoreContext = new ValueTask(new Source(true), 1); + AwaitConfigDefault(vtIgnoreContext).GetAwaiter().GetResult(); + Assert.Equal("UseSchedulingContext", trace); + AwaitConfigTrue(vtIgnoreContext).GetAwaiter().GetResult(); + Assert.Equal("UseSchedulingContext", trace); + AwaitConfigFalse(vtIgnoreContext).GetAwaiter().GetResult(); + Assert.Equal("None", trace); + + Console.WriteLine(); + SynchronizationContext.SetSynchronizationContext(new SideeffectingContext()); + + AwaitConfigDefault(vtUseContext).GetAwaiter().GetResult(); + Assert.Equal("UseSchedulingContext Posted", trace); + AwaitConfigTrue(vtUseContext).GetAwaiter().GetResult(); + Assert.Equal("UseSchedulingContext Posted", trace); + AwaitConfigFalse(vtUseContext).GetAwaiter().GetResult(); + Assert.Equal("None Posted", trace); + + AwaitConfigDefault(vtIgnoreContext).GetAwaiter().GetResult(); + Assert.Equal("UseSchedulingContext Posted", trace); + AwaitConfigTrue(vtIgnoreContext).GetAwaiter().GetResult(); + Assert.Equal("UseSchedulingContext Posted", trace); + AwaitConfigFalse(vtIgnoreContext).GetAwaiter().GetResult(); + Assert.Equal("None Posted", trace); + } + + class SideeffectingContext : SynchronizationContext + { + public override void Post(SendOrPostCallback d, object? state) + { + trace += " Posted"; + base.Post(d, state); + } + } + + static async ValueTask AwaitConfigDefault(ValueTask vt) + { + await (vt); + } + + static async ValueTask AwaitConfigTrue(ValueTask vt) + { + await (vt).ConfigureAwait(true); + } + + static async ValueTask AwaitConfigFalse(ValueTask vt) + { + await (vt).ConfigureAwait(false); + } + + class Source : IValueTaskSource + { + private readonly bool _useContext; + public Source(bool useContext) + { + _useContext = useContext; + } + + public void GetResult(short token) + { + } + + public ValueTaskSourceStatus GetStatus(short token) + { + return ValueTaskSourceStatus.Pending; + } + + public void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + { + trace = flags.ToString(); + + var schedulingContext = SynchronizationContext.Current; + if (_useContext && schedulingContext != null) + { + schedulingContext.Post((obj) => continuation(obj), state); + } + else + { + ThreadPool.QueueUserWorkItem((obj) => continuation(obj), state); + } + } + } +} diff --git a/src/tests/async/valuetask Source/valuetaskSource.csproj b/src/tests/async/valuetask Source/valuetaskSource.csproj new file mode 100644 index 00000000000000..9367a79b2edbb1 --- /dev/null +++ b/src/tests/async/valuetask Source/valuetaskSource.csproj @@ -0,0 +1,8 @@ + + + True + + + + +