From 2784aadb026463eb5b1189b912369f359a5008e3 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 31 Mar 2021 09:30:24 -0400 Subject: [PATCH] Add AsyncMethodBuilderOverride and PoolingAsyncValueTaskMethodBuilders (#50116) * Add AsyncMethodBuilderOverride and PoolingAsyncValueTaskMethodBuilders * Revise based on C# LDM changes to model * Fix API compat errors --- .../System.Private.CoreLib.Shared.projitems | 2 + .../AsyncMethodBuilderAttribute.cs | 5 +- .../AsyncValueTaskMethodBuilder.cs | 114 +--- .../AsyncValueTaskMethodBuilderT.cs | 433 +------------ .../PoolingAsyncValueTaskMethodBuilder.cs | 121 ++++ .../PoolingAsyncValueTaskMethodBuilderT.cs | 473 ++++++++++++++ .../System.Runtime/ref/System.Runtime.cs | 29 +- .../tests/AsyncValueTaskMethodBuilderTests.cs | 63 -- ...PoolingAsyncValueTaskMethodBuilderTests.cs | 579 ++++++++++++++++++ ...em.Threading.Tasks.Extensions.Tests.csproj | 1 + .../ApiCompatBaseline.PreviousNetCoreApp.txt | 3 +- ...patBaseline.netcoreapp.netstandardOnly.txt | 3 +- 12 files changed, 1242 insertions(+), 584 deletions(-) create mode 100644 src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilder.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.cs create mode 100644 src/libraries/System.Threading.Tasks.Extensions/tests/PoolingAsyncValueTaskMethodBuilderTests.cs diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 0bd2bbe668584..ecdd5da8509fa 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -693,6 +693,8 @@ + + diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderAttribute.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderAttribute.cs index 8c65d2f04173d..026e43c24a45f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderAttribute.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderAttribute.cs @@ -5,9 +5,10 @@ namespace System.Runtime.CompilerServices { /// /// Indicates the type of the async method builder that should be used by a language compiler to - /// build the attributed type when used as the return type of an async method. + /// build the attributed async method or to build the attributed type when used as the return type + /// of an async method. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface | AttributeTargets.Delegate | AttributeTargets.Enum, Inherited = false, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface | AttributeTargets.Delegate | AttributeTargets.Enum | AttributeTargets.Method, Inherited = false, AllowMultiple = false)] public sealed class AsyncMethodBuilderAttribute : Attribute { /// Initializes the . diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncValueTaskMethodBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncValueTaskMethodBuilder.cs index d725b51ca051b..bab1cdf3376e2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncValueTaskMethodBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncValueTaskMethodBuilder.cs @@ -3,9 +3,6 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; -using Internal.Runtime.CompilerServices; - -using StateMachineBox = System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder.StateMachineBox; namespace System.Runtime.CompilerServices { @@ -13,23 +10,14 @@ namespace System.Runtime.CompilerServices [StructLayout(LayoutKind.Auto)] public struct AsyncValueTaskMethodBuilder { - /// true if we should use reusable boxes for async completions of ValueTask methods; false if we should use tasks. - /// - /// We rely on tiered compilation turning this into a const and doing dead code elimination to make checks on this efficient. - /// It's also required for safety that this value never changes once observed, as Unsafe.As casts are employed based on its value. - /// - internal static readonly bool s_valueTaskPoolingEnabled = GetPoolAsyncValueTasksSwitch(); - /// Maximum number of boxes that are allowed to be cached per state machine type. - internal static readonly int s_valueTaskPoolingCacheSize = GetPoolAsyncValueTasksLimitValue(); - /// Sentinel object used to indicate that the builder completed synchronously and successfully. - private static readonly object s_syncSuccessSentinel = AsyncValueTaskMethodBuilder.s_syncSuccessSentinel; + private static readonly Task s_syncSuccessSentinel = AsyncValueTaskMethodBuilder.s_syncSuccessSentinel; - /// The wrapped state machine box or task, based on the value of . + /// The wrapped task. /// /// If the operation completed synchronously and successfully, this will be . /// - private object? m_task; // Debugger depends on the exact name of this field. + private Task? m_task; // Debugger depends on the exact name of this field. /// Creates an instance of the struct. /// The initialized instance. @@ -39,8 +27,7 @@ public struct AsyncValueTaskMethodBuilder /// The type of the state machine. /// The state machine instance, passed by reference. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Start(ref TStateMachine stateMachine) - where TStateMachine : IAsyncStateMachine => + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => AsyncMethodBuilderCore.Start(ref stateMachine); /// Associates the builder with the specified state machine. @@ -55,29 +42,16 @@ public void SetResult() { m_task = s_syncSuccessSentinel; } - else if (s_valueTaskPoolingEnabled) - { - Unsafe.As(m_task).SetResult(default); - } else { - AsyncTaskMethodBuilder.SetExistingTaskResult(Unsafe.As>(m_task), default); + AsyncTaskMethodBuilder.SetExistingTaskResult(m_task, default); } } /// Marks the task as failed and binds the specified exception to the task. /// The exception to bind to the task. - public void SetException(Exception exception) - { - if (s_valueTaskPoolingEnabled) - { - AsyncValueTaskMethodBuilder.SetException(exception, ref Unsafe.As(ref m_task)); - } - else - { - AsyncTaskMethodBuilder.SetException(exception, ref Unsafe.As?>(ref m_task)); - } - } + public void SetException(Exception exception) => + AsyncTaskMethodBuilder.SetException(exception, ref m_task); /// Gets the task for this builder. public ValueTask Task @@ -94,27 +68,11 @@ public ValueTask Task // or it should be completing asynchronously, in which case AwaitUnsafeOnCompleted would have similarly // initialized m_task to a state machine object. However, if the type is used manually (not via // compiler-generated code) and accesses Task directly, we force it to be initialized. Things will then - // "work" but in a degraded mode, as we don't know the TStateMachine type here, and thus we use a box around - // the interface instead. + // "work" but in a degraded mode, as we don't know the TStateMachine type here, and thus we use a normal + // task object instead. - if (s_valueTaskPoolingEnabled) - { - var box = Unsafe.As(m_task); - if (box is null) - { - m_task = box = AsyncValueTaskMethodBuilder.CreateWeaklyTypedStateMachineBox(); - } - return new ValueTask(box, box.Version); - } - else - { - var task = Unsafe.As?>(m_task); - if (task is null) - { - m_task = task = new Task(); // base task used rather than box to minimize size when used as manual promise - } - return new ValueTask(task); - } + Task? task = m_task ??= new Task(); // base task used rather than box to minimize size when used as manual promise + return new ValueTask(task); } } @@ -125,17 +83,8 @@ public ValueTask Task /// The state machine. public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion - where TStateMachine : IAsyncStateMachine - { - if (s_valueTaskPoolingEnabled) - { - AsyncValueTaskMethodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine, ref Unsafe.As(ref m_task)); - } - else - { - AsyncTaskMethodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine, ref Unsafe.As?>(ref m_task)); - } - } + where TStateMachine : IAsyncStateMachine => + AsyncTaskMethodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine, ref m_task); /// Schedules the state machine to proceed to the next action when the specified awaiter completes. /// The type of the awaiter. @@ -145,17 +94,8 @@ public void AwaitOnCompleted(ref TAwaiter awaiter, ref [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine - { - if (s_valueTaskPoolingEnabled) - { - AsyncValueTaskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref Unsafe.As(ref m_task)); - } - else - { - AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref Unsafe.As?>(ref m_task)); - } - } + where TStateMachine : IAsyncStateMachine => + AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task); /// /// Gets an object that may be used to uniquely identify this builder to the debugger. @@ -165,28 +105,6 @@ public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter /// It must only be used by the debugger and tracing purposes, and only in a single-threaded manner /// when no other threads are in the middle of accessing this or other members that lazily initialize the box. /// - internal object ObjectIdForDebugger - { - get - { - if (m_task is null) - { - m_task = s_valueTaskPoolingEnabled ? (object) - AsyncValueTaskMethodBuilder.CreateWeaklyTypedStateMachineBox() : - AsyncTaskMethodBuilder.CreateWeaklyTypedStateMachineBox(); - } - - return m_task; - } - } - - private static bool GetPoolAsyncValueTasksSwitch() => - Environment.GetEnvironmentVariable("DOTNET_SYSTEM_THREADING_POOLASYNCVALUETASKS") is string value && - (bool.IsTrueStringIgnoreCase(value) || value == "1"); - - private static int GetPoolAsyncValueTasksLimitValue() => - int.TryParse(Environment.GetEnvironmentVariable("DOTNET_SYSTEM_THREADING_POOLASYNCVALUETASKSLIMIT"), out int result) && result > 0 ? - result : - Environment.ProcessorCount * 4; // arbitrary default value + internal object ObjectIdForDebugger => m_task ??= AsyncTaskMethodBuilder.CreateWeaklyTypedStateMachineBox(); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncValueTaskMethodBuilderT.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncValueTaskMethodBuilderT.cs index 856feb8d95a9a..62499d54e761b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncValueTaskMethodBuilderT.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncValueTaskMethodBuilderT.cs @@ -1,13 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; -using System.Threading; using System.Threading.Tasks; -using System.Threading.Tasks.Sources; -using Internal.Runtime.CompilerServices; namespace System.Runtime.CompilerServices { @@ -22,12 +17,10 @@ public struct AsyncValueTaskMethodBuilder /// is valid for the mode in which we're operating. As such, it's cached on the generic builder per TResult /// rather than having one sentinel instance for all types. /// - internal static readonly object s_syncSuccessSentinel = AsyncValueTaskMethodBuilder.s_valueTaskPoolingEnabled ? (object) - new SyncSuccessSentinelStateMachineBox() : - new Task(default(TResult)!); + internal static readonly Task s_syncSuccessSentinel = new Task(default(TResult)!); - /// The wrapped state machine or task. If the operation completed synchronously and successfully, this will be a sentinel object compared by reference identity. - private object? m_task; // Debugger depends on the exact name of this field. + /// The wrapped task. If the operation completed synchronously and successfully, this will be a sentinel object compared by reference identity. + private Task? m_task; // Debugger depends on the exact name of this field. /// The result for this builder if it's completed synchronously, in which case will be . private TResult _result; @@ -56,39 +49,16 @@ public void SetResult(TResult result) _result = result; m_task = s_syncSuccessSentinel; } - else if (AsyncValueTaskMethodBuilder.s_valueTaskPoolingEnabled) - { - Unsafe.As(m_task).SetResult(result); - } else { - AsyncTaskMethodBuilder.SetExistingTaskResult(Unsafe.As>(m_task), result); + AsyncTaskMethodBuilder.SetExistingTaskResult(m_task, result); } } /// Marks the value task as failed and binds the specified exception to the value task. /// The exception to bind to the value task. - public void SetException(Exception exception) - { - if (AsyncValueTaskMethodBuilder.s_valueTaskPoolingEnabled) - { - SetException(exception, ref Unsafe.As(ref m_task)); - } - else - { - AsyncTaskMethodBuilder.SetException(exception, ref Unsafe.As?>(ref m_task)); - } - } - - internal static void SetException(Exception exception, [NotNull] ref StateMachineBox? boxFieldRef) - { - if (exception is null) - { - ThrowHelper.ThrowArgumentNullException(ExceptionArgument.exception); - } - - (boxFieldRef ??= CreateWeaklyTypedStateMachineBox()).SetException(exception); - } + public void SetException(Exception exception) => + AsyncTaskMethodBuilder.SetException(exception, ref m_task); /// Gets the value task for this builder. public ValueTask Task @@ -105,27 +75,11 @@ public ValueTask Task // or it should be completing asynchronously, in which case AwaitUnsafeOnCompleted would have similarly // initialized m_task to a state machine object. However, if the type is used manually (not via // compiler-generated code) and accesses Task directly, we force it to be initialized. Things will then - // "work" but in a degraded mode, as we don't know the TStateMachine type here, and thus we use a box around - // the interface instead. + // "work" but in a degraded mode, as we don't know the TStateMachine type here, and thus we use a + // normal task object instead. - if (AsyncValueTaskMethodBuilder.s_valueTaskPoolingEnabled) - { - var box = Unsafe.As(m_task); - if (box is null) - { - m_task = box = CreateWeaklyTypedStateMachineBox(); - } - return new ValueTask(box, box.Version); - } - else - { - var task = Unsafe.As?>(m_task); - if (task is null) - { - m_task = task = new Task(); // base task used rather than box to minimize size when used as manual promise - } - return new ValueTask(task); - } + Task? task = m_task ??= new Task(); // base task used rather than box to minimize size when used as manual promise + return new ValueTask(task); } } @@ -136,32 +90,8 @@ public ValueTask Task /// The state machine. public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion - where TStateMachine : IAsyncStateMachine - { - if (AsyncValueTaskMethodBuilder.s_valueTaskPoolingEnabled) - { - AwaitOnCompleted(ref awaiter, ref stateMachine, ref Unsafe.As(ref m_task)); - } - else - { - AsyncTaskMethodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine, ref Unsafe.As?>(ref m_task)); - } - } - - internal static void AwaitOnCompleted( - ref TAwaiter awaiter, ref TStateMachine stateMachine, ref StateMachineBox? box) - where TAwaiter : INotifyCompletion - where TStateMachine : IAsyncStateMachine - { - try - { - awaiter.OnCompleted(GetStateMachineBox(ref stateMachine, ref box).MoveNextAction); - } - catch (Exception e) - { - System.Threading.Tasks.Task.ThrowAsync(e, targetContext: null); - } - } + where TStateMachine : IAsyncStateMachine => + AsyncTaskMethodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine, ref m_task); /// Schedules the state machine to proceed to the next action when the specified awaiter completes. /// The type of the awaiter. @@ -171,106 +101,8 @@ internal static void AwaitOnCompleted( [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine - { - if (AsyncValueTaskMethodBuilder.s_valueTaskPoolingEnabled) - { - AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref Unsafe.As(ref m_task)); - } - else - { - AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref Unsafe.As?>(ref m_task)); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void AwaitUnsafeOnCompleted( - ref TAwaiter awaiter, ref TStateMachine stateMachine, [NotNull] ref StateMachineBox? boxRef) - where TAwaiter : ICriticalNotifyCompletion - where TStateMachine : IAsyncStateMachine - { - IAsyncStateMachineBox box = GetStateMachineBox(ref stateMachine, ref boxRef); - AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, box); - } - - /// Gets the "boxed" state machine object. - /// Specifies the type of the async state machine. - /// The state machine. - /// A reference to the field containing the initialized state machine box. - /// The "boxed" state machine. - private static IAsyncStateMachineBox GetStateMachineBox( - ref TStateMachine stateMachine, - [NotNull] ref StateMachineBox? boxFieldRef) - where TStateMachine : IAsyncStateMachine - { - ExecutionContext? currentContext = ExecutionContext.Capture(); - - // Check first for the most common case: not the first yield in an async method. - // In this case, the first yield will have already "boxed" the state machine in - // a strongly-typed manner into an AsyncStateMachineBox. It will already contain - // the state machine as well as a MoveNextDelegate and a context. The only thing - // we might need to do is update the context if that's changed since it was stored. - if (boxFieldRef is StateMachineBox stronglyTypedBox) - { - if (stronglyTypedBox.Context != currentContext) - { - stronglyTypedBox.Context = currentContext; - } - - return stronglyTypedBox; - } - - // The least common case: we have a weakly-typed boxed. This results if the debugger - // or some other use of reflection accesses a property like ObjectIdForDebugger. In - // such situations, we need to get an object to represent the builder, but we don't yet - // know the type of the state machine, and thus can't use TStateMachine. Instead, we - // use the IAsyncStateMachine interface, which all TStateMachines implement. This will - // result in a boxing allocation when storing the TStateMachine if it's a struct, but - // this only happens in active debugging scenarios where such performance impact doesn't - // matter. - if (boxFieldRef is StateMachineBox weaklyTypedBox) - { - // If this is the first await, we won't yet have a state machine, so store it. - if (weaklyTypedBox.StateMachine is null) - { - Debugger.NotifyOfCrossThreadDependency(); // same explanation as with usage below - weaklyTypedBox.StateMachine = stateMachine; - } - - // Update the context. This only happens with a debugger, so no need to spend - // extra IL checking for equality before doing the assignment. - weaklyTypedBox.Context = currentContext; - return weaklyTypedBox; - } - - // Alert a listening debugger that we can't make forward progress unless it slips threads. - // If we don't do this, and a method that uses "await foo;" is invoked through funceval, - // we could end up hooking up a callback to push forward the async method's state machine, - // the debugger would then abort the funceval after it takes too long, and then continuing - // execution could result in another callback being hooked up. At that point we have - // multiple callbacks registered to push the state machine, which could result in bad behavior. - Debugger.NotifyOfCrossThreadDependency(); - - // At this point, m_task should really be null, in which case we want to create the box. - // However, in a variety of debugger-related (erroneous) situations, it might be non-null, - // e.g. if the Task property is examined in a Watch window, forcing it to be lazily-intialized - // as a Task rather than as an ValueTaskStateMachineBox. The worst that happens in such - // cases is we lose the ability to properly step in the debugger, as the debugger uses that - // object's identity to track this specific builder/state machine. As such, we proceed to - // overwrite whatever's there anyway, even if it's non-null. - var box = StateMachineBox.GetOrCreateBox(); - boxFieldRef = box; // important: this must be done before storing stateMachine into box.StateMachine! - box.StateMachine = stateMachine; - box.Context = currentContext; - - return box; - } - - /// - /// Creates a box object for use when a non-standard access pattern is employed, e.g. when Task - /// is evaluated in the debugger prior to the async method yielding for the first time. - /// - internal static StateMachineBox CreateWeaklyTypedStateMachineBox() => new StateMachineBox(); + where TStateMachine : IAsyncStateMachine => + AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task); /// /// Gets an object that may be used to uniquely identify this builder to the debugger. @@ -280,241 +112,6 @@ private static IAsyncStateMachineBox GetStateMachineBox( /// It must only be used by the debugger and tracing purposes, and only in a single-threaded manner /// when no other threads are in the middle of accessing this or other members that lazily initialize the box. /// - internal object ObjectIdForDebugger - { - get - { - if (m_task is null) - { - m_task = AsyncValueTaskMethodBuilder.s_valueTaskPoolingEnabled ? (object) - CreateWeaklyTypedStateMachineBox() : - AsyncTaskMethodBuilder.CreateWeaklyTypedStateMachineBox(); - } - - return m_task; - } - } - - /// The base type for all value task box reusable box objects, regardless of state machine type. - internal abstract class StateMachineBox : - IValueTaskSource, IValueTaskSource - { - /// A delegate to the MoveNext method. - protected Action? _moveNextAction; - /// Captured ExecutionContext with which to invoke MoveNext. - public ExecutionContext? Context; - /// Implementation for IValueTaskSource interfaces. - protected ManualResetValueTaskSourceCore _valueTaskSource; - - /// Completes the box with a result. - /// The result. - public void SetResult(TResult result) => - _valueTaskSource.SetResult(result); - - /// Completes the box with an error. - /// The exception. - public void SetException(Exception error) => - _valueTaskSource.SetException(error); - - /// Gets the status of the box. - public ValueTaskSourceStatus GetStatus(short token) => _valueTaskSource.GetStatus(token); - - /// Schedules the continuation action for this box. - public void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => - _valueTaskSource.OnCompleted(continuation, state, token, flags); - - /// Gets the current version number of the box. - public short Version => _valueTaskSource.Version; - - /// Implemented by derived type. - TResult IValueTaskSource.GetResult(short token) => throw NotImplemented.ByDesign; - - /// Implemented by derived type. - void IValueTaskSource.GetResult(short token) => throw NotImplemented.ByDesign; - } - - private sealed class SyncSuccessSentinelStateMachineBox : StateMachineBox - { - public SyncSuccessSentinelStateMachineBox() => SetResult(default!); - } - - /// Provides a strongly-typed box object based on the specific state machine type in use. - private sealed class StateMachineBox : - StateMachineBox, - IValueTaskSource, IValueTaskSource, IAsyncStateMachineBox, IThreadPoolWorkItem - where TStateMachine : IAsyncStateMachine - { - /// Delegate used to invoke on an ExecutionContext when passed an instance of this box type. - private static readonly ContextCallback s_callback = ExecutionContextCallback; - /// Lock used to protected the shared cache of boxes. - /// The code that uses this assumes a runtime without thread aborts. - private static int s_cacheLock; - /// Singly-linked list cache of boxes. - private static StateMachineBox? s_cache; - /// The number of items stored in . - private static int s_cacheSize; - - // TODO: - // AsyncTaskMethodBuilder logs about the state machine box lifecycle; AsyncValueTaskMethodBuilder currently - // does not when it employs these pooled boxes. That logging is based on Task IDs, which we lack here. - // We could use the box's Version, but that is very likely to conflict with the IDs of other tasks in the system. - // For now, we don't log, but should we choose to we'll probably want to store an int ID on the state machine box, - // and initialize it an ID from Task's generator. - - /// If this box is stored in the cache, the next box in the cache. - private StateMachineBox? _next; - /// The state machine itself. - public TStateMachine? StateMachine; - - /// Gets a box object to use for an operation. This may be a reused, pooled object, or it may be new. - [MethodImpl(MethodImplOptions.AggressiveInlining)] // only one caller - internal static StateMachineBox GetOrCreateBox() - { - // Try to acquire the lock to access the cache. If there's any contention, don't use the cache. - if (Interlocked.CompareExchange(ref s_cacheLock, 1, 0) == 0) - { - // If there are any instances cached, take one from the cache stack and use it. - StateMachineBox? box = s_cache; - if (!(box is null)) - { - s_cache = box._next; - box._next = null; - s_cacheSize--; - Debug.Assert(s_cacheSize >= 0, "Expected the cache size to be non-negative."); - - // Release the lock and return the box. - Volatile.Write(ref s_cacheLock, 0); - return box; - } - - // No objects were cached. We'll just create a new instance. - Debug.Assert(s_cacheSize == 0, "Expected cache size to be 0."); - - // Release the lock. - Volatile.Write(ref s_cacheLock, 0); - } - - // Couldn't quickly get a cached instance, so create a new instance. - return new StateMachineBox(); - } - - private void ReturnOrDropBox() - { - Debug.Assert(_next is null, "Expected box to not be part of cached list."); - - // Clear out the state machine and associated context to avoid keeping arbitrary state referenced by - // lifted locals. We want to do this regardless of whether we end up caching the box or not, in case - // the caller keeps the box alive for an arbitrary period of time. - ClearStateUponCompletion(); - - // Reset the MRVTSC. We can either do this here, in which case we may be paying the (small) overhead - // to reset the box even if we're going to drop it, or we could do it while holding the lock, in which - // case we'll only reset it if necessary but causing the lock to be held for longer, thereby causing - // more contention. For now at least, we do it outside of the lock. (This must not be done after - // the lock is released, since at that point the instance could already be in use elsewhere.) - // We also want to increment the version number even if we're going to drop it, to maximize the chances - // that incorrectly double-awaiting a ValueTask will produce an error. - _valueTaskSource.Reset(); - - // If reusing the object would result in potentially wrapping around its version number, just throw it away. - // This provides a modicum of additional safety when ValueTasks are misused (helping to avoid the case where - // a ValueTask is illegally re-awaited and happens to do so at exactly 2^16 uses later on this exact same instance), - // at the expense of potentially incurring an additional allocation every 65K uses. - if ((ushort)_valueTaskSource.Version == ushort.MaxValue) - { - return; - } - - // Try to acquire the cache lock. If there's any contention, or if the cache is full, we just throw away the object. - if (Interlocked.CompareExchange(ref s_cacheLock, 1, 0) == 0) - { - if (s_cacheSize < AsyncValueTaskMethodBuilder.s_valueTaskPoolingCacheSize) - { - // Push the box onto the cache stack for subsequent reuse. - _next = s_cache; - s_cache = this; - s_cacheSize++; - Debug.Assert(s_cacheSize > 0 && s_cacheSize <= AsyncValueTaskMethodBuilder.s_valueTaskPoolingCacheSize, "Expected cache size to be within bounds."); - } - - // Release the lock. - Volatile.Write(ref s_cacheLock, 0); - } - } - - /// - /// Clear out the state machine and associated context to avoid keeping arbitrary state referenced by lifted locals. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ClearStateUponCompletion() - { - StateMachine = default; - Context = default; - } - - /// - /// Used to initialize s_callback above. We don't use a lambda for this on purpose: a lambda would - /// introduce a new generic type behind the scenes that comes with a hefty size penalty in AOT builds. - /// - private static void ExecutionContextCallback(object? s) - { - // Only used privately to pass directly to EC.Run - Debug.Assert(s is StateMachineBox); - Unsafe.As>(s).StateMachine!.MoveNext(); - } - - /// A delegate to the method. - public Action MoveNextAction => _moveNextAction ??= new Action(MoveNext); - - /// Invoked to run MoveNext when this instance is executed from the thread pool. - void IThreadPoolWorkItem.Execute() => MoveNext(); - - /// Calls MoveNext on - public void MoveNext() - { - ExecutionContext? context = Context; - - if (context is null) - { - Debug.Assert(!(StateMachine is null)); - StateMachine.MoveNext(); - } - else - { - ExecutionContext.RunInternal(context, s_callback, this); - } - } - - /// Get the result of the operation. - TResult IValueTaskSource.GetResult(short token) - { - try - { - return _valueTaskSource.GetResult(token); - } - finally - { - // Reuse this instance if possible, otherwise clear and drop it. - ReturnOrDropBox(); - } - } - - /// Get the result of the operation. - void IValueTaskSource.GetResult(short token) - { - try - { - _valueTaskSource.GetResult(token); - } - finally - { - // Reuse this instance if possible, otherwise clear and drop it. - ReturnOrDropBox(); - } - } - - /// Gets the state machine as a boxed object. This should only be used for debugging purposes. - IAsyncStateMachine IAsyncStateMachineBox.GetStateMachineObject() => StateMachine!; // likely boxes, only use for debugging - } + internal object ObjectIdForDebugger => m_task ??= AsyncTaskMethodBuilder.CreateWeaklyTypedStateMachineBox(); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilder.cs new file mode 100644 index 0000000000000..84996a0e062fd --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilder.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +using StateMachineBox = System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder.StateMachineBox; + +namespace System.Runtime.CompilerServices +{ + /// Represents a builder for asynchronous methods that return a . + [StructLayout(LayoutKind.Auto)] + public struct PoolingAsyncValueTaskMethodBuilder + { + /// Maximum number of boxes that are allowed to be cached per state machine type. + internal static readonly int s_valueTaskPoolingCacheSize = + int.TryParse(Environment.GetEnvironmentVariable("DOTNET_SYSTEM_THREADING_POOLINGASYNCVALUETASKSCACHESIZE"), NumberStyles.Integer, CultureInfo.InvariantCulture, out int result) && result > 0 ? + result : + Environment.ProcessorCount * 4; // arbitrary default value + + /// Sentinel object used to indicate that the builder completed synchronously and successfully. + private static readonly StateMachineBox s_syncSuccessSentinel = PoolingAsyncValueTaskMethodBuilder.s_syncSuccessSentinel; + + /// The wrapped state machine box. + /// + /// If the operation completed synchronously and successfully, this will be . + /// + private StateMachineBox? m_task; // Debugger depends on the exact name of this field. + + /// Creates an instance of the struct. + /// The initialized instance. + public static PoolingAsyncValueTaskMethodBuilder Create() => default; + + /// Begins running the builder with the associated state machine. + /// The type of the state machine. + /// The state machine instance, passed by reference. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Start(ref TStateMachine stateMachine) + where TStateMachine : IAsyncStateMachine => + AsyncMethodBuilderCore.Start(ref stateMachine); + + /// Associates the builder with the specified state machine. + /// The state machine instance to associate with the builder. + public void SetStateMachine(IAsyncStateMachine stateMachine) => + AsyncMethodBuilderCore.SetStateMachine(stateMachine, task: null); + + /// Marks the task as successfully completed. + public void SetResult() + { + if (m_task is null) + { + m_task = s_syncSuccessSentinel; + } + else + { + m_task.SetResult(default); + } + } + + /// Marks the task as failed and binds the specified exception to the task. + /// The exception to bind to the task. + public void SetException(Exception exception) => + PoolingAsyncValueTaskMethodBuilder.SetException(exception, ref m_task); + + /// Gets the task for this builder. + public ValueTask Task + { + get + { + if (m_task == s_syncSuccessSentinel) + { + return default; + } + + // With normal access paterns, m_task should always be non-null here: the async method should have + // either completed synchronously, in which case SetResult would have set m_task to a non-null object, + // or it should be completing asynchronously, in which case AwaitUnsafeOnCompleted would have similarly + // initialized m_task to a state machine object. However, if the type is used manually (not via + // compiler-generated code) and accesses Task directly, we force it to be initialized. Things will then + // "work" but in a degraded mode, as we don't know the TStateMachine type here, and thus we use a box around + // the interface instead. + + StateMachineBox? box = m_task ??= PoolingAsyncValueTaskMethodBuilder.CreateWeaklyTypedStateMachineBox(); + return new ValueTask(box, box.Version); + } + } + + /// Schedules the state machine to proceed to the next action when the specified awaiter completes. + /// The type of the awaiter. + /// The type of the state machine. + /// The awaiter. + /// The state machine. + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine => + PoolingAsyncValueTaskMethodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine, ref m_task); + + /// Schedules the state machine to proceed to the next action when the specified awaiter completes. + /// The type of the awaiter. + /// The type of the state machine. + /// The awaiter. + /// The state machine. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine => + PoolingAsyncValueTaskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task); + + /// + /// Gets an object that may be used to uniquely identify this builder to the debugger. + /// + /// + /// This property lazily instantiates the ID in a non-thread-safe manner. + /// It must only be used by the debugger and tracing purposes, and only in a single-threaded manner + /// when no other threads are in the middle of accessing this or other members that lazily initialize the box. + /// + internal object ObjectIdForDebugger => + m_task ??= PoolingAsyncValueTaskMethodBuilder.CreateWeaklyTypedStateMachineBox(); + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.cs new file mode 100644 index 0000000000000..02d15bb92c3b9 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.cs @@ -0,0 +1,473 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; +using Internal.Runtime.CompilerServices; + +namespace System.Runtime.CompilerServices +{ + /// Represents a builder for asynchronous methods that returns a . + /// The type of the result. + [StructLayout(LayoutKind.Auto)] + public struct PoolingAsyncValueTaskMethodBuilder + { + /// Sentinel object used to indicate that the builder completed synchronously and successfully. + /// + /// To avoid memory safety issues even in the face of invalid race conditions, we ensure that the type of this object + /// is valid for the mode in which we're operating. As such, it's cached on the generic builder per TResult + /// rather than having one sentinel instance for all types. + /// + internal static readonly StateMachineBox s_syncSuccessSentinel = new SyncSuccessSentinelStateMachineBox(); + + /// The wrapped state machine or task. If the operation completed synchronously and successfully, this will be a sentinel object compared by reference identity. + private StateMachineBox? m_task; // Debugger depends on the exact name of this field. + /// The result for this builder if it's completed synchronously, in which case will be . + private TResult _result; + + /// Creates an instance of the struct. + /// The initialized instance. + public static PoolingAsyncValueTaskMethodBuilder Create() => default; + + /// Begins running the builder with the associated state machine. + /// The type of the state machine. + /// The state machine instance, passed by reference. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => + AsyncMethodBuilderCore.Start(ref stateMachine); + + /// Associates the builder with the specified state machine. + /// The state machine instance to associate with the builder. + public void SetStateMachine(IAsyncStateMachine stateMachine) => + AsyncMethodBuilderCore.SetStateMachine(stateMachine, task: null); + + /// Marks the value task as successfully completed. + /// The result to use to complete the value task. + public void SetResult(TResult result) + { + if (m_task is null) + { + _result = result; + m_task = s_syncSuccessSentinel; + } + else + { + m_task.SetResult(result); + } + } + + /// Marks the value task as failed and binds the specified exception to the value task. + /// The exception to bind to the value task. + public void SetException(Exception exception) => + SetException(exception, ref m_task); + + internal static void SetException(Exception exception, [NotNull] ref StateMachineBox? boxFieldRef) + { + if (exception is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.exception); + } + + (boxFieldRef ??= CreateWeaklyTypedStateMachineBox()).SetException(exception); + } + + /// Gets the value task for this builder. + public ValueTask Task + { + get + { + if (m_task == s_syncSuccessSentinel) + { + return new ValueTask(_result); + } + + // With normal access paterns, m_task should always be non-null here: the async method should have + // either completed synchronously, in which case SetResult would have set m_task to a non-null object, + // or it should be completing asynchronously, in which case AwaitUnsafeOnCompleted would have similarly + // initialized m_task to a state machine object. However, if the type is used manually (not via + // compiler-generated code) and accesses Task directly, we force it to be initialized. Things will then + // "work" but in a degraded mode, as we don't know the TStateMachine type here, and thus we use a box around + // the interface instead. + + PoolingAsyncValueTaskMethodBuilder.StateMachineBox? box = m_task ??= CreateWeaklyTypedStateMachineBox(); + return new ValueTask(box, box.Version); + } + } + + /// Schedules the state machine to proceed to the next action when the specified awaiter completes. + /// The type of the awaiter. + /// The type of the state machine. + /// the awaiter + /// The state machine. + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine => + AwaitOnCompleted(ref awaiter, ref stateMachine, ref m_task); + + internal static void AwaitOnCompleted( + ref TAwaiter awaiter, ref TStateMachine stateMachine, ref StateMachineBox? box) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine + { + try + { + awaiter.OnCompleted(GetStateMachineBox(ref stateMachine, ref box).MoveNextAction); + } + catch (Exception e) + { + System.Threading.Tasks.Task.ThrowAsync(e, targetContext: null); + } + } + + /// Schedules the state machine to proceed to the next action when the specified awaiter completes. + /// The type of the awaiter. + /// The type of the state machine. + /// the awaiter + /// The state machine. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine => + AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void AwaitUnsafeOnCompleted( + ref TAwaiter awaiter, ref TStateMachine stateMachine, [NotNull] ref StateMachineBox? boxRef) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + { + IAsyncStateMachineBox box = GetStateMachineBox(ref stateMachine, ref boxRef); + AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, box); + } + + /// Gets the "boxed" state machine object. + /// Specifies the type of the async state machine. + /// The state machine. + /// A reference to the field containing the initialized state machine box. + /// The "boxed" state machine. + private static IAsyncStateMachineBox GetStateMachineBox( + ref TStateMachine stateMachine, + [NotNull] ref StateMachineBox? boxFieldRef) + where TStateMachine : IAsyncStateMachine + { + ExecutionContext? currentContext = ExecutionContext.Capture(); + + // Check first for the most common case: not the first yield in an async method. + // In this case, the first yield will have already "boxed" the state machine in + // a strongly-typed manner into an AsyncStateMachineBox. It will already contain + // the state machine as well as a MoveNextDelegate and a context. The only thing + // we might need to do is update the context if that's changed since it was stored. + if (boxFieldRef is StateMachineBox stronglyTypedBox) + { + if (stronglyTypedBox.Context != currentContext) + { + stronglyTypedBox.Context = currentContext; + } + + return stronglyTypedBox; + } + + // The least common case: we have a weakly-typed boxed. This results if the debugger + // or some other use of reflection accesses a property like ObjectIdForDebugger. In + // such situations, we need to get an object to represent the builder, but we don't yet + // know the type of the state machine, and thus can't use TStateMachine. Instead, we + // use the IAsyncStateMachine interface, which all TStateMachines implement. This will + // result in a boxing allocation when storing the TStateMachine if it's a struct, but + // this only happens in active debugging scenarios where such performance impact doesn't + // matter. + if (boxFieldRef is StateMachineBox weaklyTypedBox) + { + // If this is the first await, we won't yet have a state machine, so store it. + if (weaklyTypedBox.StateMachine is null) + { + Debugger.NotifyOfCrossThreadDependency(); // same explanation as with usage below + weaklyTypedBox.StateMachine = stateMachine; + } + + // Update the context. This only happens with a debugger, so no need to spend + // extra IL checking for equality before doing the assignment. + weaklyTypedBox.Context = currentContext; + return weaklyTypedBox; + } + + // Alert a listening debugger that we can't make forward progress unless it slips threads. + // If we don't do this, and a method that uses "await foo;" is invoked through funceval, + // we could end up hooking up a callback to push forward the async method's state machine, + // the debugger would then abort the funceval after it takes too long, and then continuing + // execution could result in another callback being hooked up. At that point we have + // multiple callbacks registered to push the state machine, which could result in bad behavior. + Debugger.NotifyOfCrossThreadDependency(); + + // At this point, m_task should really be null, in which case we want to create the box. + // However, in a variety of debugger-related (erroneous) situations, it might be non-null, + // e.g. if the Task property is examined in a Watch window, forcing it to be lazily-intialized + // as a Task rather than as an ValueTaskStateMachineBox. The worst that happens in such + // cases is we lose the ability to properly step in the debugger, as the debugger uses that + // object's identity to track this specific builder/state machine. As such, we proceed to + // overwrite whatever's there anyway, even if it's non-null. + StateMachineBox box = StateMachineBox.GetOrCreateBox(); + boxFieldRef = box; // important: this must be done before storing stateMachine into box.StateMachine! + box.StateMachine = stateMachine; + box.Context = currentContext; + + return box; + } + + /// + /// Creates a box object for use when a non-standard access pattern is employed, e.g. when Task + /// is evaluated in the debugger prior to the async method yielding for the first time. + /// + internal static StateMachineBox CreateWeaklyTypedStateMachineBox() => new StateMachineBox(); + + /// + /// Gets an object that may be used to uniquely identify this builder to the debugger. + /// + /// + /// This property lazily instantiates the ID in a non-thread-safe manner. + /// It must only be used by the debugger and tracing purposes, and only in a single-threaded manner + /// when no other threads are in the middle of accessing this or other members that lazily initialize the box. + /// + internal object ObjectIdForDebugger => m_task ??= CreateWeaklyTypedStateMachineBox(); + + /// The base type for all value task box reusable box objects, regardless of state machine type. + internal abstract class StateMachineBox : IValueTaskSource, IValueTaskSource + { + /// A delegate to the MoveNext method. + protected Action? _moveNextAction; + /// Captured ExecutionContext with which to invoke MoveNext. + public ExecutionContext? Context; + /// Implementation for IValueTaskSource interfaces. + protected ManualResetValueTaskSourceCore _valueTaskSource; + + /// Completes the box with a result. + /// The result. + public void SetResult(TResult result) => + _valueTaskSource.SetResult(result); + + /// Completes the box with an error. + /// The exception. + public void SetException(Exception error) => + _valueTaskSource.SetException(error); + + /// Gets the status of the box. + public ValueTaskSourceStatus GetStatus(short token) => _valueTaskSource.GetStatus(token); + + /// Schedules the continuation action for this box. + public void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => + _valueTaskSource.OnCompleted(continuation, state, token, flags); + + /// Gets the current version number of the box. + public short Version => _valueTaskSource.Version; + + /// Implemented by derived type. + TResult IValueTaskSource.GetResult(short token) => throw NotImplemented.ByDesign; + + /// Implemented by derived type. + void IValueTaskSource.GetResult(short token) => throw NotImplemented.ByDesign; + } + + /// Type used as a singleton to indicate synchronous success for an async method. + private sealed class SyncSuccessSentinelStateMachineBox : StateMachineBox + { + public SyncSuccessSentinelStateMachineBox() => SetResult(default!); + } + + /// Provides a strongly-typed box object based on the specific state machine type in use. + private sealed class StateMachineBox : + StateMachineBox, + IValueTaskSource, IValueTaskSource, IAsyncStateMachineBox, IThreadPoolWorkItem + where TStateMachine : IAsyncStateMachine + { + /// Delegate used to invoke on an ExecutionContext when passed an instance of this box type. + private static readonly ContextCallback s_callback = ExecutionContextCallback; + /// Thread-local cache of boxes. This currently only ever stores one. + [ThreadStatic] + private static StateMachineBox? t_tlsCache; + /// Lock used to protected the shared cache of boxes. 1 == held, 0 == not held. + /// The code that uses this assumes a runtime without thread aborts. + private static int s_cacheLock; + /// Singly-linked list cache of boxes. + private static StateMachineBox? s_cache; + /// The number of items stored in . + private static int s_cacheSize; + + /// If this box is stored in the cache, the next box in the cache. + private StateMachineBox? _next; + /// The state machine itself. + public TStateMachine? StateMachine; + + /// Gets a box object to use for an operation. This may be a reused, pooled object, or it may be new. + [MethodImpl(MethodImplOptions.AggressiveInlining)] // only one caller + internal static StateMachineBox GetOrCreateBox() + { + StateMachineBox? box; + + // First see if the thread-static cache of at most one box has one. + box = t_tlsCache; + if (box is not null) + { + t_tlsCache = null; + return box; + } + + // Try to acquire the lock to access the cache. If there's any contention, don't use the cache. + if (s_cache is not null && // hot read just to see if there's any point paying for the interlocked + Interlocked.Exchange(ref s_cacheLock, 1) == 0) + { + // If there are any instances cached, take one from the cache stack and use it. + box = s_cache; + if (box is not null) + { + s_cache = box._next; + box._next = null; + s_cacheSize--; + Debug.Assert(s_cacheSize >= 0, "Expected the cache size to be non-negative."); + + // Release the lock and return the box. + Volatile.Write(ref s_cacheLock, 0); + return box; + } + + // No objects were cached. We'll just create a new instance. + Debug.Assert(s_cacheSize == 0, "Expected cache size to be 0."); + + // Release the lock. + Volatile.Write(ref s_cacheLock, 0); + } + + // Couldn't quickly get a cached instance, so create a new instance. + return new StateMachineBox(); + } + + /// Returns this instance to the cache, or drops it if the cache is full or this instance shouldn't be cached. + private void ReturnOrDropBox() + { + Debug.Assert(_next is null, "Expected box to not be part of cached list."); + + // Clear out the state machine and associated context to avoid keeping arbitrary state referenced by + // lifted locals. We want to do this regardless of whether we end up caching the box or not, in case + // the caller keeps the box alive for an arbitrary period of time. + ClearStateUponCompletion(); + + // Reset the MRVTSC. We can either do this here, in which case we may be paying the (small) overhead + // to reset the box even if we're going to drop it, or we could do it while holding the lock, in which + // case we'll only reset it if necessary but causing the lock to be held for longer, thereby causing + // more contention. For now at least, we do it outside of the lock. (This must not be done after + // the lock is released, since at that point the instance could already be in use elsewhere.) + // We also want to increment the version number even if we're going to drop it, to maximize the chances + // that incorrectly double-awaiting a ValueTask will produce an error. + _valueTaskSource.Reset(); + + // If reusing the object would result in potentially wrapping around its version number, just throw it away. + // This provides a modicum of additional safety when ValueTasks are misused (helping to avoid the case where + // a ValueTask is illegally re-awaited and happens to do so at exactly 2^16 uses later on this exact same instance), + // at the expense of potentially incurring an additional allocation every 65K uses. + if ((ushort)_valueTaskSource.Version == ushort.MaxValue) + { + return; + } + + // If the thread static cache is empty, store this into it and bail. + if (t_tlsCache is null) + { + t_tlsCache = this; + return; + } + + // Try to acquire the cache lock. If there's any contention, or if the cache is full, we just throw away the object. + if (Interlocked.Exchange(ref s_cacheLock, 1) == 0) + { + if (s_cacheSize < PoolingAsyncValueTaskMethodBuilder.s_valueTaskPoolingCacheSize) + { + // Push the box onto the cache stack for subsequent reuse. + _next = s_cache; + s_cache = this; + s_cacheSize++; + Debug.Assert(s_cacheSize > 0 && s_cacheSize <= PoolingAsyncValueTaskMethodBuilder.s_valueTaskPoolingCacheSize, "Expected cache size to be within bounds."); + } + + // Release the lock. + Volatile.Write(ref s_cacheLock, 0); + } + } + + /// + /// Clear out the state machine and associated context to avoid keeping arbitrary state referenced by lifted locals. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearStateUponCompletion() + { + StateMachine = default; + Context = default; + } + + /// + /// Used to initialize s_callback above. We don't use a lambda for this on purpose: a lambda would + /// introduce a new generic type behind the scenes that comes with a hefty size penalty in AOT builds. + /// + private static void ExecutionContextCallback(object? s) + { + // Only used privately to pass directly to EC.Run + Debug.Assert(s is StateMachineBox); + Unsafe.As>(s).StateMachine!.MoveNext(); + } + + /// A delegate to the method. + public Action MoveNextAction => _moveNextAction ??= new Action(MoveNext); + + /// Invoked to run MoveNext when this instance is executed from the thread pool. + void IThreadPoolWorkItem.Execute() => MoveNext(); + + /// Calls MoveNext on + public void MoveNext() + { + ExecutionContext? context = Context; + + if (context is null) + { + Debug.Assert(!(StateMachine is null)); + StateMachine.MoveNext(); + } + else + { + ExecutionContext.RunInternal(context, s_callback, this); + } + } + + /// Get the result of the operation. + TResult IValueTaskSource.GetResult(short token) + { + try + { + return _valueTaskSource.GetResult(token); + } + finally + { + // Reuse this instance if possible, otherwise clear and drop it. + ReturnOrDropBox(); + } + } + + /// Get the result of the operation. + void IValueTaskSource.GetResult(short token) + { + try + { + _valueTaskSource.GetResult(token); + } + finally + { + // Reuse this instance if possible, otherwise clear and drop it. + ReturnOrDropBox(); + } + } + + /// Gets the state machine as a boxed object. This should only be used for debugging purposes. + IAsyncStateMachine IAsyncStateMachineBox.GetStateMachineObject() => StateMachine!; // likely boxes, only use for debugging + } + } +} diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 5e3c282aff8ac..303016c562438 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -9114,7 +9114,7 @@ public sealed partial class AsyncIteratorStateMachineAttribute : System.Runtime. { public AsyncIteratorStateMachineAttribute(System.Type stateMachineType) : base (default(System.Type)) { } } - [System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Delegate | System.AttributeTargets.Enum | System.AttributeTargets.Interface | System.AttributeTargets.Struct, Inherited=false, AllowMultiple=false)] + [System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Delegate | System.AttributeTargets.Enum | System.AttributeTargets.Interface | System.AttributeTargets.Method | System.AttributeTargets.Struct, Inherited=false, AllowMultiple=false)] public sealed partial class AsyncMethodBuilderAttribute : System.Attribute { public AsyncMethodBuilderAttribute(System.Type builderType) { } @@ -9524,6 +9524,33 @@ public sealed partial class ModuleInitializerAttribute : System.Attribute { public ModuleInitializerAttribute() { } } + public partial struct PoolingAsyncValueTaskMethodBuilder + { + private object _dummy; + private int _dummyPrimitive; + public System.Threading.Tasks.ValueTask Task { get { throw null; } } + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : System.Runtime.CompilerServices.INotifyCompletion where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine { } + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine { } + public static System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder Create() { throw null; } + public void SetException(System.Exception exception) { } + public void SetResult() { } + public void SetStateMachine(System.Runtime.CompilerServices.IAsyncStateMachine stateMachine) { } + public void Start(ref TStateMachine stateMachine) where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine { } + } + public partial struct PoolingAsyncValueTaskMethodBuilder + { + private TResult _result; + private object _dummy; + private int _dummyPrimitive; + public System.Threading.Tasks.ValueTask Task { get { throw null; } } + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : System.Runtime.CompilerServices.INotifyCompletion where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine { } + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine { } + public static System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder Create() { throw null; } + public void SetException(System.Exception exception) { } + public void SetResult(TResult result) { } + public void SetStateMachine(System.Runtime.CompilerServices.IAsyncStateMachine stateMachine) { } + public void Start(ref TStateMachine stateMachine) where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine { } + } [System.AttributeUsageAttribute(System.AttributeTargets.Method, AllowMultiple=false, Inherited=false)] public sealed partial class PreserveBaseOverridesAttribute : System.Attribute { diff --git a/src/libraries/System.Threading.Tasks.Extensions/tests/AsyncValueTaskMethodBuilderTests.cs b/src/libraries/System.Threading.Tasks.Extensions/tests/AsyncValueTaskMethodBuilderTests.cs index d929d30d22d64..e515a1ff0a7e2 100644 --- a/src/libraries/System.Threading.Tasks.Extensions/tests/AsyncValueTaskMethodBuilderTests.cs +++ b/src/libraries/System.Threading.Tasks.Extensions/tests/AsyncValueTaskMethodBuilderTests.cs @@ -555,69 +555,6 @@ static async ValueTask ValueTaskAsync(int i) })); } - [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [InlineData("1", null)] - [InlineData("true", null)] - [InlineData("true", "1")] - [InlineData("true", "100")] - [InlineData("false", null)] - [InlineData("false", "100")] - public void PoolingAsyncValueTasksBuilder_ObjectsPooled(string poolingEnvVar, string limitEnvVar) - { - // Use RemoteExecutor to launch a process with the right environment variables set - var psi = new ProcessStartInfo(); - psi.Environment.Add("DOTNET_SYSTEM_THREADING_POOLASYNCVALUETASKS", poolingEnvVar); - if (limitEnvVar != null) - { - psi.Environment.Add("DOTNET_SYSTEM_THREADING_POOLASYNCVALUETASKSLIMIT", limitEnvVar); - } - - RemoteExecutor.Invoke(async expectReuse => - { - var boxes = new ConcurrentQueue(); - var valueTasks = new ValueTask[10]; - int total = 0; - - // Invoke a bunch of ValueTask methods, some in parallel, - // and track a) their results and b) what boxing object is used. - for (int rep = 0; rep < 3; rep++) - { - for (int i = 0; i < valueTasks.Length; i++) - { - valueTasks[i] = ComputeAsync(i + 1, boxes); - } - foreach (ValueTask vt in valueTasks) - { - total += await vt; - } - } - - // Make sure we got the right total, and that if we expected pooling, - // we at least pooled one object. - Assert.Equal(330, total); - if (expectReuse == "1" || expectReuse == "true") - { - Assert.InRange(boxes.Distinct().Count(), 1, boxes.Count - 1); - } - }, (poolingEnvVar == "1" || poolingEnvVar == "true").ToString(), new RemoteInvokeOptions() { StartInfo = psi }).Dispose(); - - static async ValueTask ComputeAsync(int input, ConcurrentQueue boxes) - { - await RecursiveValueTaskAsync(3, boxes); - return input * 2; - } - - static async ValueTask RecursiveValueTaskAsync(int depth, ConcurrentQueue boxes) - { - boxes.Enqueue(await GetStateMachineData.FetchAsync()); - if (depth > 0) - { - await Task.Delay(1); - await RecursiveValueTaskAsync(depth - 1, boxes); - } - } - } - private struct DelegateStateMachine : IAsyncStateMachine { internal Action MoveNextDelegate; diff --git a/src/libraries/System.Threading.Tasks.Extensions/tests/PoolingAsyncValueTaskMethodBuilderTests.cs b/src/libraries/System.Threading.Tasks.Extensions/tests/PoolingAsyncValueTaskMethodBuilderTests.cs new file mode 100644 index 0000000000000..c2e83881ce5b1 --- /dev/null +++ b/src/libraries/System.Threading.Tasks.Extensions/tests/PoolingAsyncValueTaskMethodBuilderTests.cs @@ -0,0 +1,579 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks.Sources.Tests; +using Microsoft.DotNet.RemoteExecutor; +using Xunit; + +namespace System.Threading.Tasks.Tests +{ + public class PoolingAsyncValueTaskMethodBuilderTests + { + [Fact] + public void Create_ReturnsDefaultInstance() // implementation detail being verified + { + Assert.Equal(default, PoolingAsyncValueTaskMethodBuilder.Create()); + Assert.Equal(default, PoolingAsyncValueTaskMethodBuilder.Create()); + } + + [Fact] + public void NonGeneric_SetResult_BeforeAccessTask_ValueTaskIsDefault() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + + b.SetResult(); + + Assert.Equal(default, b.Task); + } + + [Fact] + public void Generic_SetResult_BeforeAccessTask_ValueTaskContainsValue() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + + b.SetResult(42); + + ValueTask vt = b.Task; + Assert.Equal(vt, b.Task); + Assert.Equal(new ValueTask(42), vt); + } + + [Fact] + public void NonGeneric_SetResult_AfterAccessTask_ValueTaskContainsValue() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + + ValueTask vt = b.Task; + Assert.NotEqual(default, vt); + Assert.Equal(vt, b.Task); + + b.SetResult(); + + Assert.Equal(vt, b.Task); + Assert.True(vt.IsCompletedSuccessfully); + } + + [Fact] + public void Generic_SetResult_AfterAccessTask_ValueTaskContainsValue() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + + ValueTask vt = b.Task; + Assert.NotEqual(default, vt); + Assert.Equal(vt, b.Task); + + b.SetResult(42); + + Assert.Equal(vt, b.Task); + Assert.True(vt.IsCompletedSuccessfully); + Assert.Equal(42, vt.Result); + } + + [Fact] + public void NonGeneric_SetException_BeforeAccessTask_FaultsTask() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + + var e = new FormatException(); + b.SetException(e); + + ValueTask vt = b.Task; + Assert.Equal(vt, b.Task); + Assert.True(vt.IsFaulted); + Assert.Same(e, Assert.Throws(() => vt.GetAwaiter().GetResult())); + } + + [Fact] + public void Generic_SetException_BeforeAccessTask_FaultsTask() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + + var e = new FormatException(); + b.SetException(e); + + ValueTask vt = b.Task; + Assert.Equal(vt, b.Task); + Assert.True(vt.IsFaulted); + Assert.Same(e, Assert.Throws(() => vt.GetAwaiter().GetResult())); + } + + [Fact] + public void NonGeneric_SetException_AfterAccessTask_FaultsTask() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + + ValueTask vt = b.Task; + Assert.Equal(vt, b.Task); + + var e = new FormatException(); + b.SetException(e); + + Assert.Equal(vt, b.Task); + Assert.True(vt.IsFaulted); + Assert.Same(e, Assert.Throws(() => vt.GetAwaiter().GetResult())); + } + + [Fact] + public void Generic_SetException_AfterAccessTask_FaultsTask() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + + ValueTask vt = b.Task; + Assert.Equal(vt, b.Task); + + var e = new FormatException(); + b.SetException(e); + + Assert.Equal(vt, b.Task); + Assert.True(vt.IsFaulted); + Assert.Same(e, Assert.Throws(() => vt.GetAwaiter().GetResult())); + } + + [Fact] + public void NonGeneric_SetException_OperationCanceledException_CancelsTask() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + + ValueTask vt = b.Task; + Assert.Equal(vt, b.Task); + + var e = new OperationCanceledException(); + b.SetException(e); + + Assert.Equal(vt, b.Task); + Assert.True(vt.IsCanceled); + Assert.Same(e, Assert.Throws(() => vt.GetAwaiter().GetResult())); + } + + [Fact] + public void Generic_SetException_OperationCanceledException_CancelsTask() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + + ValueTask vt = b.Task; + Assert.Equal(vt, b.Task); + + var e = new OperationCanceledException(); + b.SetException(e); + + Assert.Equal(vt, b.Task); + Assert.True(vt.IsCanceled); + Assert.Same(e, Assert.Throws(() => vt.GetAwaiter().GetResult())); + } + + [Fact] + public void NonGeneric_SetExceptionWithNullException_Throws() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + AssertExtensions.Throws("exception", () => b.SetException(null)); + } + + [Fact] + public void Generic_SetExceptionWithNullException_Throws() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + AssertExtensions.Throws("exception", () => b.SetException(null)); + } + + [Fact] + public void NonGeneric_Start_InvokesMoveNext() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + + int invokes = 0; + var dsm = new DelegateStateMachine { MoveNextDelegate = () => invokes++ }; + b.Start(ref dsm); + + Assert.Equal(1, invokes); + } + + [Fact] + public void Generic_Start_InvokesMoveNext() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + + int invokes = 0; + var dsm = new DelegateStateMachine { MoveNextDelegate = () => invokes++ }; + b.Start(ref dsm); + + Assert.Equal(1, invokes); + } + + [Theory] + [InlineData(1, false)] + [InlineData(2, false)] + [InlineData(1, true)] + [InlineData(2, true)] + public void NonGeneric_AwaitOnCompleted_ForcesTaskCreation(int numAwaits, bool awaitUnsafe) + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + + var dsm = new DelegateStateMachine(); + TaskAwaiter t = new TaskCompletionSource().Task.GetAwaiter(); + + Assert.InRange(numAwaits, 1, int.MaxValue); + for (int i = 1; i <= numAwaits; i++) + { + if (awaitUnsafe) + { + b.AwaitUnsafeOnCompleted(ref t, ref dsm); + } + else + { + b.AwaitOnCompleted(ref t, ref dsm); + } + } + + b.SetResult(); + + ValueTask vt = b.Task; + Assert.NotEqual(default, vt); + Assert.True(vt.IsCompletedSuccessfully); + } + + [Theory] + [InlineData(1, false)] + [InlineData(2, false)] + [InlineData(1, true)] + [InlineData(2, true)] + public void Generic_AwaitOnCompleted_ForcesTaskCreation(int numAwaits, bool awaitUnsafe) + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + + var dsm = new DelegateStateMachine(); + TaskAwaiter t = new TaskCompletionSource().Task.GetAwaiter(); + + Assert.InRange(numAwaits, 1, int.MaxValue); + for (int i = 1; i <= numAwaits; i++) + { + if (awaitUnsafe) + { + b.AwaitUnsafeOnCompleted(ref t, ref dsm); + } + else + { + b.AwaitOnCompleted(ref t, ref dsm); + } + } + + b.SetResult(42); + + ValueTask vt = b.Task; + Assert.NotEqual(default, vt); + Assert.True(vt.IsCompletedSuccessfully); + Assert.Equal(42, vt.Result); + } + + [Fact] + public void NonGeneric_SetStateMachine_InvalidArgument_ThrowsException() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + AssertExtensions.Throws("stateMachine", () => b.SetStateMachine(null)); + } + + [Fact] + public void Generic_SetStateMachine_InvalidArgument_ThrowsException() + { + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + AssertExtensions.Throws("stateMachine", () => b.SetStateMachine(null)); + } + + [Fact] + public void NonGeneric_Start_ExecutionContextChangesInMoveNextDontFlowOut() + { + var al = new AsyncLocal { Value = 0 }; + int calls = 0; + + var dsm = new DelegateStateMachine + { + MoveNextDelegate = () => + { + al.Value++; + calls++; + } + }; + + dsm.MoveNext(); + Assert.Equal(1, al.Value); + Assert.Equal(1, calls); + + dsm.MoveNext(); + Assert.Equal(2, al.Value); + Assert.Equal(2, calls); + + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + b.Start(ref dsm); + Assert.Equal(2, al.Value); // change should not be visible + Assert.Equal(3, calls); + + // Make sure we've not caused the Task to be allocated + b.SetResult(); + Assert.Equal(default, b.Task); + } + + [Fact] + public void Generic_Start_ExecutionContextChangesInMoveNextDontFlowOut() + { + var al = new AsyncLocal { Value = 0 }; + int calls = 0; + + var dsm = new DelegateStateMachine + { + MoveNextDelegate = () => + { + al.Value++; + calls++; + } + }; + + dsm.MoveNext(); + Assert.Equal(1, al.Value); + Assert.Equal(1, calls); + + dsm.MoveNext(); + Assert.Equal(2, al.Value); + Assert.Equal(2, calls); + + PoolingAsyncValueTaskMethodBuilder b = PoolingAsyncValueTaskMethodBuilder.Create(); + b.Start(ref dsm); + Assert.Equal(2, al.Value); // change should not be visible + Assert.Equal(3, calls); + + // Make sure we've not caused the Task to be allocated + b.SetResult(42); + Assert.Equal(new ValueTask(42), b.Task); + } + + [ActiveIssue("https://github.com/dotnet/roslyn/issues/51999")] + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(10)] + public static async Task NonGeneric_UsedWithAsyncMethod_CompletesSuccessfully(int yields) + { + StrongBox result; + + result = new StrongBox(); + await ValueTaskReturningAsyncMethod(42, result); + Assert.Equal(42 + yields, result.Value); + + result = new StrongBox(); + await ValueTaskReturningAsyncMethod(84, result); + Assert.Equal(84 + yields, result.Value); + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + async ValueTask ValueTaskReturningAsyncMethod(int result, StrongBox output) + { + for (int i = 0; i < yields; i++) + { + await Task.Yield(); + result++; + } + output.Value = result; + } + } + + [ActiveIssue("https://github.com/dotnet/roslyn/issues/51999")] + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(10)] + public static async Task Generic_UsedWithAsyncMethod_CompletesSuccessfully(int yields) + { + Assert.Equal(42 + yields, await ValueTaskReturningAsyncMethod(42)); + Assert.Equal(84 + yields, await ValueTaskReturningAsyncMethod(84)); + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + async ValueTask ValueTaskReturningAsyncMethod(int result) + { + for (int i = 0; i < yields; i++) + { + await Task.Yield(); + result++; + } + return result; + } + } + + [ActiveIssue("https://github.com/dotnet/roslyn/issues/51999")] + [Fact] + public static async Task AwaitTasksAndValueTasks_InTaskAndValueTaskMethods() + { + for (int i = 0; i < 2; i++) + { + await ValueTaskReturningMethod(); + Assert.Equal(18, await ValueTaskInt32ReturningMethod()); + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + async ValueTask ValueTaskReturningMethod() + { + for (int i = 0; i < 3; i++) + { + // Complete + await Task.CompletedTask; + await Task.FromResult(42); + await new ValueTask(); + await Assert.ThrowsAsync(async () => await new ValueTask(Task.FromException(new FormatException()))); + await Assert.ThrowsAsync(async () => await new ValueTask(ManualResetValueTaskSourceFactory.Completed(0, new FormatException()), 0)); + Assert.Equal(42, await new ValueTask(42)); + Assert.Equal(42, await new ValueTask(Task.FromResult(42))); + Assert.Equal(42, await new ValueTask(ManualResetValueTaskSourceFactory.Completed(42, null), 0)); + await Assert.ThrowsAsync(async () => await new ValueTask(Task.FromException(new FormatException()))); + await Assert.ThrowsAsync(async () => await new ValueTask(ManualResetValueTaskSourceFactory.Completed(0, new FormatException()), 0)); + + // Incomplete + await Assert.ThrowsAsync(async () => await new ValueTask(Task.Delay(1).ContinueWith(_ => throw new FormatException()))); + await Assert.ThrowsAsync(async () => await new ValueTask(ManualResetValueTaskSourceFactory.Delay(1, 0, new FormatException()), 0)); + Assert.Equal(42, await new ValueTask(Task.Delay(1).ContinueWith(_ => 42))); + Assert.Equal(42, await new ValueTask(ManualResetValueTaskSourceFactory.Delay(1, 42, null), 0)); + await Assert.ThrowsAsync(async () => await new ValueTask(Task.Delay(1).ContinueWith(_ => throw new FormatException()))); + await Assert.ThrowsAsync(async () => await new ValueTask(ManualResetValueTaskSourceFactory.Delay(1, 0, new FormatException()), 0)); + await Task.Yield(); + } + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + async ValueTask ValueTaskInt32ReturningMethod() + { + for (int i = 0; i < 3; i++) + { + // Complete + await Task.CompletedTask; + await Task.FromResult(42); + await new ValueTask(); + await Assert.ThrowsAsync(async () => await new ValueTask(Task.FromException(new FormatException()))); + await Assert.ThrowsAsync(async () => await new ValueTask(ManualResetValueTaskSourceFactory.Completed(0, new FormatException()), 0)); + Assert.Equal(42, await new ValueTask(42)); + Assert.Equal(42, await new ValueTask(Task.FromResult(42))); + Assert.Equal(42, await new ValueTask(ManualResetValueTaskSourceFactory.Completed(42, null), 0)); + await Assert.ThrowsAsync(async () => await new ValueTask(Task.FromException(new FormatException()))); + await Assert.ThrowsAsync(async () => await new ValueTask(ManualResetValueTaskSourceFactory.Completed(0, new FormatException()), 0)); + + // Incomplete + await Assert.ThrowsAsync(async () => await new ValueTask(Task.Delay(1).ContinueWith(_ => throw new FormatException()))); + await Assert.ThrowsAsync(async () => await new ValueTask(ManualResetValueTaskSourceFactory.Delay(1, 0, new FormatException()), 0)); + Assert.Equal(42, await new ValueTask(Task.Delay(1).ContinueWith(_ => 42))); + Assert.Equal(42, await new ValueTask(ManualResetValueTaskSourceFactory.Delay(1, 42, null), 0)); + await Assert.ThrowsAsync(async () => await new ValueTask(Task.Delay(1).ContinueWith(_ => throw new FormatException()))); + await Assert.ThrowsAsync(async () => await new ValueTask(ManualResetValueTaskSourceFactory.Delay(1, 0, new FormatException()), 0)); + await Task.Yield(); + } + return 18; + } + } + + [ActiveIssue("https://github.com/dotnet/roslyn/issues/51999")] + [Fact] + public async Task NonGeneric_ConcurrentBuilders_WorkCorrectly() + { + await Task.WhenAll(Enumerable.Range(0, Environment.ProcessorCount).Select(async _ => + { + for (int i = 0; i < 10; i++) + { + await ValueTaskAsync(); + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + static async ValueTask ValueTaskAsync() + { + await Task.Delay(1); + } + } + })); + } + + [ActiveIssue("https://github.com/dotnet/roslyn/issues/51999")] + [Fact] + public async Task Generic_ConcurrentBuilders_WorkCorrectly() + { + await Task.WhenAll(Enumerable.Range(0, Environment.ProcessorCount).Select(async _ => + { + for (int i = 0; i < 10; i++) + { + Assert.Equal(42 + i, await ValueTaskAsync(i)); + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + static async ValueTask ValueTaskAsync(int i) + { + await Task.Delay(1); + return 42 + i; + } + } + })); + } + + [ActiveIssue("https://github.com/dotnet/roslyn/issues/51999")] + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(null)] + [InlineData("1")] + [InlineData("100")] + public void PoolingAsyncValueTasksBuilder_ObjectsPooled(string limitEnvVar) + { + // Use RemoteExecutor to launch a process with the right environment variables set + var psi = new ProcessStartInfo(); + if (limitEnvVar != null) + { + psi.Environment.Add("DOTNET_SYSTEM_THREADING_POOLINGASYNCVALUETASKSCACHESIZE", limitEnvVar); + } + + RemoteExecutor.Invoke(async () => + { + var boxes = new ConcurrentQueue(); + var valueTasks = new ValueTask[10]; + int total = 0; + + // Invoke a bunch of ValueTask methods, some in parallel, + // and track a) their results and b) what boxing object is used. + for (int rep = 0; rep < 3; rep++) + { + for (int i = 0; i < valueTasks.Length; i++) + { + valueTasks[i] = ComputeAsync(i + 1, boxes); + } + foreach (ValueTask vt in valueTasks) + { + total += await vt; + } + } + + // Make sure we got the right total, and that if we expected pooling, + // we at least pooled one object. + Assert.Equal(330, total); + Assert.InRange(boxes.Distinct().Count(), 1, boxes.Count - 1); + }, new RemoteInvokeOptions() { StartInfo = psi }).Dispose(); + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + static async ValueTask ComputeAsync(int input, ConcurrentQueue boxes) + { + await RecursiveValueTaskAsync(3, boxes); + return input * 2; + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + static async ValueTask RecursiveValueTaskAsync(int depth, ConcurrentQueue boxes) + { + boxes.Enqueue(await GetStateMachineData.FetchAsync()); + if (depth > 0) + { + await Task.Delay(1); + await RecursiveValueTaskAsync(depth - 1, boxes); + } + } + } + + private struct DelegateStateMachine : IAsyncStateMachine + { + internal Action MoveNextDelegate; + public void MoveNext() => MoveNextDelegate?.Invoke(); + + public void SetStateMachine(IAsyncStateMachine stateMachine) { } + } + } +} diff --git a/src/libraries/System.Threading.Tasks.Extensions/tests/System.Threading.Tasks.Extensions.Tests.csproj b/src/libraries/System.Threading.Tasks.Extensions/tests/System.Threading.Tasks.Extensions.Tests.csproj index 6a7215d2b5cb5..3cf17c52a2fc8 100644 --- a/src/libraries/System.Threading.Tasks.Extensions/tests/System.Threading.Tasks.Extensions.Tests.csproj +++ b/src/libraries/System.Threading.Tasks.Extensions/tests/System.Threading.Tasks.Extensions.Tests.csproj @@ -8,6 +8,7 @@ + diff --git a/src/libraries/shims/ApiCompatBaseline.PreviousNetCoreApp.txt b/src/libraries/shims/ApiCompatBaseline.PreviousNetCoreApp.txt index c9cd20297935b..401f0a5f0ba15 100644 --- a/src/libraries/shims/ApiCompatBaseline.PreviousNetCoreApp.txt +++ b/src/libraries/shims/ApiCompatBaseline.PreviousNetCoreApp.txt @@ -23,4 +23,5 @@ CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'System.Ru CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'System.Runtime.Versioning.UnsupportedOSPlatformAttribute' changed from '[AttributeUsageAttribute(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Enum | AttributeTargets.Event | AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Module | AttributeTargets.Property | AttributeTargets.Struct, AllowMultiple=true, Inherited=false)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Enum | AttributeTargets.Event | AttributeTargets.Field | AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Module | AttributeTargets.Property | AttributeTargets.Struct, AllowMultiple=true, Inherited=false)]' in the implementation. Compat issues with assembly System.Security.Cryptography.Algorithms: CannotRemoveAttribute : Attribute 'System.Runtime.Versioning.UnsupportedOSPlatformAttribute' exists on 'System.Security.Cryptography.CryptoConfig' in the contract but not the implementation. -Total Issues: 15 +CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'System.Runtime.CompilerServices.AsyncMethodBuilderAttribute' changed from '[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Delegate | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Struct, Inherited=false, AllowMultiple=false)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Delegate | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Struct, Inherited=false, AllowMultiple=false)]' in the implementation. +Total Issues: 16 diff --git a/src/libraries/shims/ApiCompatBaseline.netcoreapp.netstandardOnly.txt b/src/libraries/shims/ApiCompatBaseline.netcoreapp.netstandardOnly.txt index b5e47f7d9647f..2510e64f1e5c5 100644 --- a/src/libraries/shims/ApiCompatBaseline.netcoreapp.netstandardOnly.txt +++ b/src/libraries/shims/ApiCompatBaseline.netcoreapp.netstandardOnly.txt @@ -195,4 +195,5 @@ CannotRemoveAttribute : Attribute 'System.ComponentModel.BrowsableAttribute' exi CannotRemoveAttribute : Attribute 'System.ComponentModel.CategoryAttribute' exists on 'System.Timers.Timer.Elapsed' in the contract but not the implementation. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'System.Xml.Serialization.XmlAnyAttributeAttribute' changed from '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple=false)]' in the implementation. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'System.Xml.Serialization.XmlNamespaceDeclarationsAttribute' changed from '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple=false)]' in the implementation. -Total Issues: 196 +CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'System.Runtime.CompilerServices.AsyncMethodBuilderAttribute' changed from '[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Delegate | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Struct, Inherited=false, AllowMultiple=false)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Delegate | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Struct, Inherited=false, AllowMultiple=false)]' in the implementation. +Total Issues: 198