Skip to content

Commit da1da02

Browse files
authored
Add and use ConfigureAwaitOptions (#87067)
* Add and use ConfigureAwaitOptions Adds the ability to further control how awaits are performed, with the ConfigureAwaitOptions enum. For .NET 8 this is just for `Task` / `Task<TResult>`, and for the latter, the `SuppressThrowing` option isn't supported (as it can make the TResult erroneous). Also uses it throughout the repo to replace various custom awaiters, use of Task.Run, and places we were catching all exceptions to suppress them with awaits. Some of these just help to clean up the code; others have / enable meaningful perf improvements. * Address PR feedback * Update src/libraries/System.Net.HttpListener/src/System/Net/Managed/HttpResponseStream.Managed.cs
1 parent 7319f86 commit da1da02

32 files changed

Lines changed: 479 additions & 420 deletions

File tree

src/libraries/Common/src/System/Net/Http/X509ResourceClient.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace System.Net.Http
1212
{
1313
internal static partial class X509ResourceClient
1414
{
15-
private static readonly Func<string, CancellationToken, bool, ValueTask<byte[]?>>? s_downloadBytes = CreateDownloadBytesFunc();
15+
private static readonly Func<string, CancellationToken, bool, Task<byte[]?>>? s_downloadBytes = CreateDownloadBytesFunc();
1616

1717
static partial void ReportNoClient();
1818
static partial void ReportNegativeTimeout();
@@ -24,18 +24,17 @@ internal static partial class X509ResourceClient
2424

2525
internal static byte[]? DownloadAsset(string uri, TimeSpan downloadTimeout)
2626
{
27-
ValueTask<byte[]?> task = DownloadAssetCore(uri, downloadTimeout, async: false);
27+
Task<byte[]?> task = DownloadAssetCore(uri, downloadTimeout, async: false);
2828
Debug.Assert(task.IsCompletedSuccessfully);
2929
return task.Result;
3030
}
3131

3232
internal static Task<byte[]?> DownloadAssetAsync(string uri, TimeSpan downloadTimeout)
3333
{
34-
ValueTask<byte[]?> task = DownloadAssetCore(uri, downloadTimeout, async: true);
35-
return task.AsTask();
34+
return DownloadAssetCore(uri, downloadTimeout, async: true);
3635
}
3736

38-
private static async ValueTask<byte[]?> DownloadAssetCore(string uri, TimeSpan downloadTimeout, bool async)
37+
private static async Task<byte[]?> DownloadAssetCore(string uri, TimeSpan downloadTimeout, bool async)
3938
{
4039
if (s_downloadBytes is null)
4140
{
@@ -60,8 +59,12 @@ internal static partial class X509ResourceClient
6059

6160
try
6261
{
63-
ret = await s_downloadBytes(uri, cts?.Token ?? default, async).ConfigureAwait(false);
64-
return ret;
62+
Task<byte[]?> task = s_downloadBytes(uri, cts?.Token ?? default, async);
63+
await ((Task)task).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
64+
if (task.IsCompletedSuccessfully)
65+
{
66+
return task.Result;
67+
}
6568
}
6669
catch { }
6770
finally
@@ -74,7 +77,7 @@ internal static partial class X509ResourceClient
7477
return null;
7578
}
7679

77-
private static Func<string, CancellationToken, bool, ValueTask<byte[]?>>? CreateDownloadBytesFunc()
80+
private static Func<string, CancellationToken, bool, Task<byte[]?>>? CreateDownloadBytesFunc()
7881
{
7982
try
8083
{

src/libraries/Common/src/System/Threading/AsyncOverSyncWithIoCancellation.cs

Lines changed: 53 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace System.Threading
1414
/// <summary>
1515
/// Helper for performing asynchronous I/O on Windows implemented as queueing a work item that performs synchronous I/O, complete with cancellation support.
1616
/// </summary>
17-
internal sealed class AsyncOverSyncWithIoCancellation : IThreadPoolWorkItem, ICriticalNotifyCompletion
17+
internal sealed class AsyncOverSyncWithIoCancellation
1818
{
1919
/// <summary>A thread handle for the current OS thread.</summary>
2020
/// <remarks>This is lazily-initialized for the current OS thread. We rely on finalization to clean up after it when the thread goes away.</remarks>
@@ -32,23 +32,9 @@ internal sealed class AsyncOverSyncWithIoCancellation : IThreadPoolWorkItem, ICr
3232
/// the callback wasn't and will never be invoked. If it's non-null, its completion represents the completion of the asynchronous callback.
3333
/// </summary>
3434
private volatile Task? CallbackCompleted;
35-
/// <summary>The <see cref="Action"/> continuation object handed to this instance when used as an awaiter to scheduler work to the thread pool.</summary>
36-
private Action? _continuation;
37-
38-
// awaitable / awaiter implementation that enables this instance to be awaited in order to queue
39-
// execution to the thread pool. This is purely a cost-saving measure in order to reuse this
40-
// object we already need as the queued work item.
41-
public AsyncOverSyncWithIoCancellation GetAwaiter() => this;
42-
public bool IsCompleted => false;
43-
public void GetResult() { }
44-
public void OnCompleted(Action continuation) => throw new NotSupportedException();
45-
public void UnsafeOnCompleted(Action continuation)
46-
{
47-
Debug.Assert(_continuation is null);
48-
_continuation = continuation;
49-
ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: true);
50-
}
51-
void IThreadPoolWorkItem.Execute() => _continuation!();
35+
36+
/// <summary>Prevent external instantiation.</summary>
37+
private AsyncOverSyncWithIoCancellation() { }
5238

5339
/// <summary>Queues the invocation of <paramref name="action"/> to the thread pool.</summary>
5440
/// <typeparam name="TState">The type of the state passed to <paramref name="action"/>.</typeparam>
@@ -65,27 +51,24 @@ public void UnsafeOnCompleted(Action continuation)
6551
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))]
6652
public static async ValueTask InvokeAsync<TState>(Action<TState> action, TState state, CancellationToken cancellationToken)
6753
{
68-
// Create the work item state object. This is used to pass around state through various APIs,
69-
// while also serving double duty as the work item used to queue the operation to the thread pool.
70-
var workItem = new AsyncOverSyncWithIoCancellation();
71-
72-
// Queue the work to the thread pool. This is implemented as a custom awaiter that queues the
73-
// awaiter itself to the thread pool.
74-
await workItem;
54+
// Queue the work to complete asynchronously.
55+
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
7556

7657
// Register for cancellation, perform the work, and clean up. Even though we're in an async method, awaits _must not_ be used inside
7758
// the using block, or else the I/O cancellation could both not work and negatively interact with I/O on another thread. The func
7859
// _must_ be invoked on the same thread that invoked RegisterCancellation, with no intervening work.
79-
await using (workItem.RegisterCancellation(cancellationToken).ConfigureAwait(false))
60+
SyncAsyncWorkItemRegistration reg = RegisterCancellation(cancellationToken);
61+
try
8062
{
81-
try
82-
{
83-
action(state);
84-
}
85-
catch (OperationCanceledException oce) when (cancellationToken.IsCancellationRequested && oce.CancellationToken != cancellationToken)
86-
{
87-
throw CreateAppropriateCancellationException(cancellationToken, oce);
88-
}
63+
action(state);
64+
}
65+
catch (OperationCanceledException oce) when (cancellationToken.IsCancellationRequested && oce.CancellationToken != cancellationToken)
66+
{
67+
throw CreateAppropriateCancellationException(cancellationToken, oce);
68+
}
69+
finally
70+
{
71+
await reg.DisposeAsync().ConfigureAwait(false);
8972
}
9073
}
9174

@@ -105,27 +88,24 @@ public static async ValueTask InvokeAsync<TState>(Action<TState> action, TState
10588
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
10689
public static async ValueTask<TResult> InvokeAsync<TState, TResult>(Func<TState, TResult> func, TState state, CancellationToken cancellationToken)
10790
{
108-
// Create the work item state object. This is used to pass around state through various APIs,
109-
// while also serving double duty as the work item used to queue the operation to the thread pool.
110-
var workItem = new AsyncOverSyncWithIoCancellation();
111-
112-
// Queue the work to the thread pool. This is implemented as a custom awaiter that queues the
113-
// awaiter itself to the thread pool.
114-
await workItem;
91+
// Queue the work to complete asynchronously.
92+
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
11593

11694
// Register for cancellation, perform the work, and clean up. Even though we're in an async method, awaits _must not_ be used inside
11795
// the using block, or else the I/O cancellation could both not work and negatively interact with I/O on another thread. The func
11896
// _must_ be invoked on the same thread that invoked RegisterCancellation, with no intervening work.
119-
await using (workItem.RegisterCancellation(cancellationToken).ConfigureAwait(false))
97+
SyncAsyncWorkItemRegistration reg = RegisterCancellation(cancellationToken);
98+
try
12099
{
121-
try
122-
{
123-
return func(state);
124-
}
125-
catch (OperationCanceledException oce) when (cancellationToken.IsCancellationRequested && oce.CancellationToken != cancellationToken)
126-
{
127-
throw CreateAppropriateCancellationException(cancellationToken, oce);
128-
}
100+
return func(state);
101+
}
102+
catch (OperationCanceledException oce) when (cancellationToken.IsCancellationRequested && oce.CancellationToken != cancellationToken)
103+
{
104+
throw CreateAppropriateCancellationException(cancellationToken, oce);
105+
}
106+
finally
107+
{
108+
await reg.DisposeAsync().ConfigureAwait(false);
129109
}
130110
}
131111

@@ -145,7 +125,7 @@ private static OperationCanceledException CreateAppropriateCancellationException
145125
return newOce;
146126
}
147127

148-
/// <summary>The struct IDisposable returned from <see cref="RegisterCancellation"/> in order to clean up after the registration.</summary>
128+
/// <summary>The struct IDisposable returned from RegisterCancellation in order to clean up after the registration.</summary>
149129
private struct SyncAsyncWorkItemRegistration : IDisposable, IAsyncDisposable
150130
{
151131
public AsyncOverSyncWithIoCancellation WorkItem;
@@ -201,44 +181,44 @@ public async ValueTask DisposeAsync()
201181

202182
/// <summary>Registers for cancellation with the specified token.</summary>
203183
/// <remarks>Upon cancellation being requested, the implementation will attempt to CancelSynchronousIo for the thread calling RegisterCancellation.</remarks>
204-
private SyncAsyncWorkItemRegistration RegisterCancellation(CancellationToken cancellationToken)
205-
{
206-
// If the token can't be canceled, there's nothing to register.
207-
if (!cancellationToken.CanBeCanceled)
208-
{
209-
return default;
210-
}
184+
private static SyncAsyncWorkItemRegistration RegisterCancellation(CancellationToken cancellationToken) =>
185+
cancellationToken.CanBeCanceled ? RegisterCancellation(new AsyncOverSyncWithIoCancellation(), cancellationToken) :
186+
default; // If the token can't be canceled, there's nothing to register.
211187

188+
/// <summary>Registers for cancellation with the specified token.</summary>
189+
/// <remarks>Upon cancellation being requested, the implementation will attempt to CancelSynchronousIo for the thread calling RegisterCancellation.</remarks>
190+
private static SyncAsyncWorkItemRegistration RegisterCancellation(AsyncOverSyncWithIoCancellation instance, CancellationToken cancellationToken)
191+
{
212192
// Get a handle for the current thread. This is stored and used to cancel the I/O on this thread
213193
// in response to the cancellation token having cancellation requested. If the handle is invalid,
214194
// which could happen if OpenThread fails, skip attempts at cancellation. The handle needs to be
215195
// opened with THREAD_TERMINATE in order to be able to call CancelSynchronousIo.
216-
ThreadHandle = t_currentThreadHandle;
217-
if (ThreadHandle is null)
196+
instance.ThreadHandle = t_currentThreadHandle;
197+
if (instance.ThreadHandle is null)
218198
{
219-
ThreadHandle = Interop.Kernel32.OpenThread(Interop.Kernel32.THREAD_TERMINATE, bInheritHandle: false, Interop.Kernel32.GetCurrentThreadId());
220-
if (ThreadHandle.IsInvalid)
199+
instance.ThreadHandle = Interop.Kernel32.OpenThread(Interop.Kernel32.THREAD_TERMINATE, bInheritHandle: false, Interop.Kernel32.GetCurrentThreadId());
200+
if (instance.ThreadHandle.IsInvalid)
221201
{
222202
int lastError = Marshal.GetLastPInvokeError();
223203
Debug.Fail($"{nameof(Interop.Kernel32.OpenThread)} unexpectedly failed with 0x{lastError:X8}: {Marshal.GetPInvokeErrorMessage(lastError)}");
224204
return default;
225205
}
226206

227-
t_currentThreadHandle = ThreadHandle;
207+
t_currentThreadHandle = instance.ThreadHandle;
228208
}
229209

230210
// Register with the token.
231211
SyncAsyncWorkItemRegistration reg = default;
232-
reg.WorkItem = this;
212+
reg.WorkItem = instance;
233213
reg.CancellationRegistration = cancellationToken.UnsafeRegister(static s =>
234214
{
235-
var state = (AsyncOverSyncWithIoCancellation)s!;
215+
var instance = (AsyncOverSyncWithIoCancellation)s!;
236216

237217
// If cancellation was already requested when UnsafeRegister was called, it'll invoke
238218
// the callback immediately. If we allowed that to loop until cancellation was successful,
239219
// we'd deadlock, as we'd never perform the very I/O it was waiting for. As such, if
240220
// the callback is invoked prior to be ready for it, we ignore the callback.
241-
if (!state.FinishedCancellationRegistration)
221+
if (!instance.FinishedCancellationRegistration)
242222
{
243223
return;
244224
}
@@ -250,17 +230,17 @@ private SyncAsyncWorkItemRegistration RegisterCancellation(CancellationToken can
250230
// this looping synchronously, we instead queue the invocation of the looping so that it
251231
// runs asynchronously from the Cancel call. Then in order to be able to track its completion,
252232
// we store the Task representing that asynchronous work, such that cleanup can wait for the Task.
253-
state.CallbackCompleted = Task.Factory.StartNew(static s =>
233+
instance.CallbackCompleted = Task.Factory.StartNew(static s =>
254234
{
255-
var state = (AsyncOverSyncWithIoCancellation)s!;
235+
var instance = (AsyncOverSyncWithIoCancellation)s!;
256236

257237
// Cancel the I/O. If the cancellation happens too early and we haven't yet initiated
258238
// the synchronous operation, CancelSynchronousIo will fail with ERROR_NOT_FOUND, and
259239
// we'll loop to try again.
260240
SpinWait sw = default;
261-
while (state.ContinueTryingToCancel)
241+
while (instance.ContinueTryingToCancel)
262242
{
263-
if (Interop.Kernel32.CancelSynchronousIo(state.ThreadHandle!))
243+
if (Interop.Kernel32.CancelSynchronousIo(instance.ThreadHandle!))
264244
{
265245
// Successfully canceled I/O.
266246
break;
@@ -276,12 +256,12 @@ private SyncAsyncWorkItemRegistration RegisterCancellation(CancellationToken can
276256

277257
sw.SpinOnce();
278258
}
279-
}, s, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
280-
}, this);
259+
}, instance, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
260+
}, instance);
281261

282262
// Now that we've registered with the token, tell the callback it's safe to enter
283263
// its cancellation loop if the callback is invoked.
284-
FinishedCancellationRegistration = true;
264+
instance.FinishedCancellationRegistration = true;
285265

286266
// And now since cancellation may have been requested and we may have suppressed it
287267
// until the previous line, check to see if cancellation has now been requested, and

src/libraries/Common/tests/StreamConformanceTests/System/IO/StreamConformanceTests.cs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -603,15 +603,6 @@ protected sealed class CustomTaskScheduler : TaskScheduler
603603
protected override IEnumerable<Task> GetScheduledTasks() => new Task[0];
604604
}
605605

606-
protected readonly struct JumpToThreadPoolAwaiter : ICriticalNotifyCompletion
607-
{
608-
public JumpToThreadPoolAwaiter GetAwaiter() => this;
609-
public bool IsCompleted => false;
610-
public void OnCompleted(Action continuation) => ThreadPool.QueueUserWorkItem(_ => continuation());
611-
public void UnsafeOnCompleted(Action continuation) => ThreadPool.UnsafeQueueUserWorkItem(_ => continuation(), null);
612-
public void GetResult() { }
613-
}
614-
615606
protected sealed unsafe class NativeMemoryManager : MemoryManager<byte>
616607
{
617608
private readonly int _length;
@@ -2046,9 +2037,7 @@ public static IEnumerable<object[]> ReadAsync_ContinuesOnCurrentContextIfDesired
20462037
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
20472038
public virtual async Task ReadAsync_ContinuesOnCurrentSynchronizationContextIfDesired(bool flowExecutionContext, bool? continueOnCapturedContext)
20482039
{
2049-
await default(JumpToThreadPoolAwaiter); // escape xunit sync ctx
2050-
2051-
using StreamPair streams = await CreateConnectedStreamsAsync();
2040+
using StreamPair streams = await CreateConnectedStreamsAsync().ConfigureAwait(ConfigureAwaitOptions.ForceYielding /* escape xunit sync ctx */);
20522041
foreach ((Stream writeable, Stream readable) in GetReadWritePairs(streams))
20532042
{
20542043
Assert.Null(SynchronizationContext.Current);
@@ -2130,9 +2119,7 @@ public virtual async Task ReadAsync_ContinuesOnCurrentSynchronizationContextIfDe
21302119
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
21312120
public virtual async Task ReadAsync_ContinuesOnCurrentTaskSchedulerIfDesired(bool flowExecutionContext, bool? continueOnCapturedContext)
21322121
{
2133-
await default(JumpToThreadPoolAwaiter); // escape xunit sync ctx
2134-
2135-
using StreamPair streams = await CreateConnectedStreamsAsync();
2122+
using StreamPair streams = await CreateConnectedStreamsAsync().ConfigureAwait(ConfigureAwaitOptions.ForceYielding /* escape xunit sync ctx */);
21362123
foreach ((Stream writeable, Stream readable) in GetReadWritePairs(streams))
21372124
{
21382125
Assert.Null(SynchronizationContext.Current);

src/libraries/Common/tests/System/TimeProviderTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ public static void RunDelayTests(TimeProvider provider, ITestTaskFactory taskFac
321321

322322
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
323323
[MemberData(nameof(TimersProvidersWithTaskFactorData))]
324-
public static async void RunWaitAsyncTests(TimeProvider provider, ITestTaskFactory taskFactory)
324+
public static async Task RunWaitAsyncTests(TimeProvider provider, ITestTaskFactory taskFactory)
325325
{
326326
CancellationTokenSource cts = new CancellationTokenSource();
327327

@@ -377,7 +377,7 @@ public static async void RunWaitAsyncTests(TimeProvider provider, ITestTaskFacto
377377
#if !NETFRAMEWORK
378378
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
379379
[MemberData(nameof(TimersProvidersListData))]
380-
public static async void PeriodicTimerTests(TimeProvider provider)
380+
public static async Task PeriodicTimerTests(TimeProvider provider)
381381
{
382382
var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1), provider);
383383
Assert.True(await timer.WaitForNextTickAsync());

0 commit comments

Comments
 (0)