@@ -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
0 commit comments