@@ -365,7 +365,7 @@ public void AwaitOnCompleted<TAwaiter, TStateMachine>(
365
365
{
366
366
try
367
367
{
368
- awaiter . OnCompleted ( GetMoveNextDelegate ( ref stateMachine ) ) ;
368
+ awaiter . OnCompleted ( GetStateMachineBox ( ref stateMachine ) . MoveNextAction ) ;
369
369
}
370
370
catch ( Exception e )
371
371
{
@@ -384,10 +384,98 @@ public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
384
384
ref TAwaiter awaiter , ref TStateMachine stateMachine )
385
385
where TAwaiter : ICriticalNotifyCompletion
386
386
where TStateMachine : IAsyncStateMachine
387
+ {
388
+ IAsyncStateMachineBox box = GetStateMachineBox ( ref stateMachine ) ;
389
+
390
+ // TODO https://github.com/dotnet/coreclr/issues/12877:
391
+ // Once the JIT is able to recognize "awaiter is ITaskAwaiter" and "awaiter is IConfiguredTaskAwaiter",
392
+ // use those in order to a) consolidate a lot of this code, and b) handle all Task/Task<T> and not just
393
+ // the few types special-cased here. For now, handle common {Configured}TaskAwaiter. Having the types
394
+ // explicitly listed here allows the JIT to generate the best code for them; otherwise we'll fall through
395
+ // to the later workaround.
396
+ if ( typeof ( TAwaiter ) == typeof ( TaskAwaiter ) ||
397
+ typeof ( TAwaiter ) == typeof ( TaskAwaiter < string > ) ||
398
+ typeof ( TAwaiter ) == typeof ( TaskAwaiter < byte [ ] > ) ||
399
+ typeof ( TAwaiter ) == typeof ( TaskAwaiter < int > ) ||
400
+ typeof ( TAwaiter ) == typeof ( TaskAwaiter < long > ) ||
401
+ typeof ( TAwaiter ) == typeof ( TaskAwaiter < byte > ) )
402
+ {
403
+ ref TaskAwaiter ta = ref Unsafe . As < TAwaiter , TaskAwaiter > ( ref awaiter ) ; // relies on TaskAwaiter/TaskAwaiter<T> having the same layout
404
+ TaskAwaiter . UnsafeOnCompletedInternal ( ta . m_task , box , continueOnCapturedContext : true ) ;
405
+ }
406
+ else if (
407
+ typeof ( TAwaiter ) == typeof ( ConfiguredTaskAwaitable . ConfiguredTaskAwaiter ) ||
408
+ typeof ( TAwaiter ) == typeof ( ConfiguredTaskAwaitable < string > . ConfiguredTaskAwaiter ) ||
409
+ typeof ( TAwaiter ) == typeof ( ConfiguredTaskAwaitable < byte [ ] > . ConfiguredTaskAwaiter ) ||
410
+ typeof ( TAwaiter ) == typeof ( ConfiguredTaskAwaitable < int > . ConfiguredTaskAwaiter ) ||
411
+ typeof ( TAwaiter ) == typeof ( ConfiguredTaskAwaitable < long > . ConfiguredTaskAwaiter ) ||
412
+ typeof ( TAwaiter ) == typeof ( ConfiguredTaskAwaitable < byte > . ConfiguredTaskAwaiter ) )
413
+ {
414
+ ref ConfiguredTaskAwaitable . ConfiguredTaskAwaiter ta = ref Unsafe . As < TAwaiter , ConfiguredTaskAwaitable . ConfiguredTaskAwaiter > ( ref awaiter ) ;
415
+ TaskAwaiter . UnsafeOnCompletedInternal ( ta . m_task , box , ta . m_continueOnCapturedContext ) ;
416
+ }
417
+
418
+ // Handle common {Configured}ValueTaskAwaiter<T> types. Unfortunately these need to be special-cased
419
+ // individually, as we don't have good way to extract the task from a ValueTaskAwaiter<T> when we don't
420
+ // know what the T is; we could make ValueTaskAwaiter<T> implement an IValueTaskAwaiter interface, but
421
+ // calling a GetTask method on that would end up boxing the awaiter. This hard-coded list here is
422
+ // somewhat arbitrary and is based on types currently in use with ValueTask<T> in coreclr/corefx.
423
+ else if ( typeof ( TAwaiter ) == typeof ( ValueTaskAwaiter < int > ) )
424
+ {
425
+ var vta = ( ValueTaskAwaiter < int > ) ( object ) awaiter ;
426
+ TaskAwaiter . UnsafeOnCompletedInternal ( vta . AsTask ( ) , box , continueOnCapturedContext : true ) ;
427
+ }
428
+ else if ( typeof ( TAwaiter ) == typeof ( ConfiguredValueTaskAwaitable < int > . ConfiguredValueTaskAwaiter ) )
429
+ {
430
+ var vta = ( ConfiguredValueTaskAwaitable < int > . ConfiguredValueTaskAwaiter ) ( object ) awaiter ;
431
+ TaskAwaiter . UnsafeOnCompletedInternal ( vta . AsTask ( ) , box , vta . _continueOnCapturedContext ) ;
432
+ }
433
+ else if ( typeof ( TAwaiter ) == typeof ( ConfiguredValueTaskAwaitable < System . IO . Stream > . ConfiguredValueTaskAwaiter ) )
434
+ {
435
+ var vta = ( ConfiguredValueTaskAwaitable < System . IO . Stream > . ConfiguredValueTaskAwaiter ) ( object ) awaiter ;
436
+ TaskAwaiter . UnsafeOnCompletedInternal ( vta . AsTask ( ) , box , vta . _continueOnCapturedContext ) ;
437
+ }
438
+ else if ( typeof ( TAwaiter ) == typeof ( ConfiguredValueTaskAwaitable < ArraySegment < byte > > . ConfiguredValueTaskAwaiter ) )
439
+ {
440
+ var vta = ( ConfiguredValueTaskAwaitable < ArraySegment < byte > > . ConfiguredValueTaskAwaiter ) ( object ) awaiter ;
441
+ TaskAwaiter . UnsafeOnCompletedInternal ( vta . AsTask ( ) , box , vta . _continueOnCapturedContext ) ;
442
+ }
443
+
444
+ // To catch all Task/Task<T> awaits, do the currently more expensive interface checks.
445
+ // Eventually these and the above Task/Task<T> checks should be replaced by "is" checks,
446
+ // once that's recognized and optimized by the JIT. We do these after all of the hardcoded
447
+ // checks above so that they don't incur the costs of these checks.
448
+ else if ( InterfaceIsCheckWorkaround < TAwaiter > . IsITaskAwaiter )
449
+ {
450
+ ref TaskAwaiter ta = ref Unsafe . As < TAwaiter , TaskAwaiter > ( ref awaiter ) ;
451
+ TaskAwaiter . UnsafeOnCompletedInternal ( ta . m_task , box , continueOnCapturedContext : true ) ;
452
+ }
453
+ else if ( InterfaceIsCheckWorkaround < TAwaiter > . IsIConfiguredTaskAwaiter )
454
+ {
455
+ ref ConfiguredTaskAwaitable . ConfiguredTaskAwaiter ta = ref Unsafe . As < TAwaiter , ConfiguredTaskAwaitable . ConfiguredTaskAwaiter > ( ref awaiter ) ;
456
+ TaskAwaiter . UnsafeOnCompletedInternal ( ta . m_task , box , ta . m_continueOnCapturedContext ) ;
457
+ }
458
+
459
+ // The awaiter isn't specially known. Fall back to doing a normal await.
460
+ else
461
+ {
462
+ // TODO: https://github.com/dotnet/coreclr/issues/14177
463
+ // Move the code back into this method once the JIT is able to
464
+ // elide it successfully when one of the previous branches is hit.
465
+ AwaitArbitraryAwaiterUnsafeOnCompleted ( ref awaiter , box ) ;
466
+ }
467
+ }
468
+
469
+ /// <summary>Schedules the specified state machine to be pushed forward when the specified awaiter completes.</summary>
470
+ /// <typeparam name="TAwaiter">Specifies the type of the awaiter.</typeparam>
471
+ /// <param name="awaiter">The awaiter.</param>
472
+ /// <param name="box">The state machine box.</param>
473
+ private static void AwaitArbitraryAwaiterUnsafeOnCompleted < TAwaiter > ( ref TAwaiter awaiter , IAsyncStateMachineBox box )
474
+ where TAwaiter : ICriticalNotifyCompletion
387
475
{
388
476
try
389
477
{
390
- awaiter . UnsafeOnCompleted ( GetMoveNextDelegate ( ref stateMachine ) ) ;
478
+ awaiter . UnsafeOnCompleted ( box . MoveNextAction ) ;
391
479
}
392
480
catch ( Exception e )
393
481
{
@@ -399,7 +487,7 @@ public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
399
487
/// <typeparam name="TStateMachine">Specifies the type of the async state machine.</typeparam>
400
488
/// <param name="stateMachine">The state machine.</param>
401
489
/// <returns>The "boxed" state machine.</returns>
402
- private Action GetMoveNextDelegate < TStateMachine > (
490
+ private IAsyncStateMachineBox GetStateMachineBox < TStateMachine > (
403
491
ref TStateMachine stateMachine )
404
492
where TStateMachine : IAsyncStateMachine
405
493
{
@@ -416,7 +504,7 @@ private Action GetMoveNextDelegate<TStateMachine>(
416
504
{
417
505
stronglyTypedBox . Context = currentContext ;
418
506
}
419
- return stronglyTypedBox . MoveNextAction ;
507
+ return stronglyTypedBox ;
420
508
}
421
509
422
510
// The least common case: we have a weakly-typed boxed. This results if the debugger
@@ -440,7 +528,7 @@ private Action GetMoveNextDelegate<TStateMachine>(
440
528
// Update the context. This only happens with a debugger, so no need to spend
441
529
// extra IL checking for equality before doing the assignment.
442
530
weaklyTypedBox . Context = currentContext ;
443
- return weaklyTypedBox . MoveNextAction ;
531
+ return weaklyTypedBox ;
444
532
}
445
533
446
534
// Alert a listening debugger that we can't make forward progress unless it slips threads.
@@ -462,34 +550,33 @@ private Action GetMoveNextDelegate<TStateMachine>(
462
550
m_task = box ; // important: this must be done before storing stateMachine into box.StateMachine!
463
551
box . StateMachine = stateMachine ;
464
552
box . Context = currentContext ;
465
- return box . MoveNextAction ;
553
+ return box ;
466
554
}
467
555
468
556
/// <summary>A strongly-typed box for Task-based async state machines.</summary>
469
557
/// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
470
558
/// <typeparam name="TResult">Specifies the type of the Task's result.</typeparam>
471
559
private sealed class AsyncStateMachineBox < TStateMachine > :
472
- Task < TResult > , IDebuggingAsyncStateMachineAccessor
560
+ Task < TResult > , IAsyncStateMachineBox
473
561
where TStateMachine : IAsyncStateMachine
474
562
{
475
563
/// <summary>Delegate used to invoke on an ExecutionContext when passed an instance of this box type.</summary>
476
564
private static readonly ContextCallback s_callback = s => ( ( AsyncStateMachineBox < TStateMachine > ) s ) . StateMachine . MoveNext ( ) ;
477
565
478
566
/// <summary>A delegate to the <see cref="MoveNext"/> method.</summary>
479
- public readonly Action MoveNextAction ;
567
+ private Action _moveNextAction ;
480
568
/// <summary>The state machine itself.</summary>
481
569
public TStateMachine StateMachine ; // mutable struct; do not make this readonly
482
570
/// <summary>Captured ExecutionContext with which to invoke <see cref="MoveNextAction"/>; may be null.</summary>
483
571
public ExecutionContext Context ;
484
572
485
- public AsyncStateMachineBox ( )
486
- {
487
- var mn = new Action ( MoveNext ) ;
488
- MoveNextAction = AsyncCausalityTracer . LoggingOn ? AsyncMethodBuilderCore . OutputAsyncCausalityEvents ( this , mn ) : mn ;
489
- }
573
+ /// <summary>A delegate to the <see cref="MoveNext"/> method.</summary>
574
+ public Action MoveNextAction =>
575
+ _moveNextAction ??
576
+ ( _moveNextAction = AsyncCausalityTracer . LoggingOn ? AsyncMethodBuilderCore . OutputAsyncCausalityEvents ( this , new Action ( MoveNext ) ) : new Action ( MoveNext ) ) ;
490
577
491
- /// <summary>Call MoveNext on <see cref="StateMachine"/>. </summary>
492
- private void MoveNext ( )
578
+ /// <summary>Calls MoveNext on <see cref="StateMachine"/></summary>
579
+ public void MoveNext ( )
493
580
{
494
581
if ( Context == null )
495
582
{
@@ -501,8 +588,19 @@ private void MoveNext()
501
588
}
502
589
}
503
590
591
+ /// <summary>
592
+ /// Calls MoveNext on <see cref="StateMachine"/>. Implements ITaskCompletionAction.Invoke so
593
+ /// that the state machine object may be queued directly as a continuation into a Task's
594
+ /// continuation slot/list.
595
+ /// </summary>
596
+ /// <param name="completedTask">The completing task that caused this method to be invoked, if there was one.</param>
597
+ void ITaskCompletionAction . Invoke ( Task completedTask ) => MoveNext ( ) ;
598
+
599
+ /// <summary>Signals to Task's continuation logic that <see cref="Invoke"/> runs arbitrary user code via MoveNext.</summary>
600
+ bool ITaskCompletionAction . InvokeMayRunArbitraryCode => true ;
601
+
504
602
/// <summary>Gets the state machine as a boxed object. This should only be used for debugging purposes.</summary>
505
- IAsyncStateMachine IDebuggingAsyncStateMachineAccessor . GetStateMachineObject ( ) => StateMachine ; // likely boxes, only use for debugging
603
+ IAsyncStateMachine IAsyncStateMachineBox . GetStateMachineObject ( ) => StateMachine ; // likely boxes, only use for debugging
506
604
}
507
605
508
606
/// <summary>Gets the <see cref="System.Threading.Tasks.Task{TResult}"/> for this builder.</summary>
@@ -815,12 +913,24 @@ internal static Task<TResult> CreateCacheableTask<TResult>(TResult result) =>
815
913
new Task < TResult > ( false , result , ( TaskCreationOptions ) InternalTaskOptions . DoNotDispose , default ( CancellationToken ) ) ;
816
914
}
817
915
916
+ /// <summary>Temporary workaround for https://github.com/dotnet/coreclr/issues/12877.</summary>
917
+ internal static class InterfaceIsCheckWorkaround < TAwaiter >
918
+ {
919
+ internal static readonly bool IsITaskAwaiter = typeof ( TAwaiter ) . GetInterface ( "ITaskAwaiter" ) != null ;
920
+ internal static readonly bool IsIConfiguredTaskAwaiter = typeof ( TAwaiter ) . GetInterface ( "IConfiguredTaskAwaiter" ) != null ;
921
+ }
922
+
818
923
/// <summary>
819
- /// An interface implemented by <see cref="AsyncStateMachineBox{TStateMachine, TResult}"/> to allow access
820
- /// non-generically to state associated with a builder and state machine.
924
+ /// An interface implemented by all <see cref="AsyncStateMachineBox{TStateMachine, TResult}"/> instances, regardless of generics.
821
925
/// </summary>
822
- interface IDebuggingAsyncStateMachineAccessor
926
+ interface IAsyncStateMachineBox : ITaskCompletionAction
823
927
{
928
+ /// <summary>
929
+ /// Gets an action for moving forward the contained state machine.
930
+ /// This will lazily-allocate the delegate as needed.
931
+ /// </summary>
932
+ Action MoveNextAction { get ; }
933
+
824
934
/// <summary>Gets the state machine as a boxed object. This should only be used for debugging purposes.</summary>
825
935
IAsyncStateMachine GetStateMachineObject ( ) ;
826
936
}
@@ -843,7 +953,7 @@ internal static Action TryGetStateMachineForDebugger(Action action) // debugger
843
953
{
844
954
object target = action . Target ;
845
955
return
846
- target is IDebuggingAsyncStateMachineAccessor sm ? sm . GetStateMachineObject ( ) . MoveNext :
956
+ target is IAsyncStateMachineBox sm ? sm . GetStateMachineObject ( ) . MoveNext :
847
957
target is ContinuationWrapper cw ? TryGetStateMachineForDebugger ( cw . _continuation ) :
848
958
action ;
849
959
}
0 commit comments