@@ -349,6 +349,29 @@ public static unsafe void MoveNext<T, TOps>(T task) where T : Task where TOps :
349349
350350 while ( true )
351351 {
352+ const CorInfoContinuationFlags continueFlags =
353+ CorInfoContinuationFlags . CORINFO_CONTINUATION_CONTINUE_ON_CAPTURED_SYNCHRONIZATION_CONTEXT |
354+ CorInfoContinuationFlags . CORINFO_CONTINUATION_CONTINUE_ON_THREAD_POOL |
355+ CorInfoContinuationFlags . CORINFO_CONTINUATION_CONTINUE_ON_CAPTURED_TASK_SCHEDULER ;
356+
357+ // Make a note if have a context-neutral continuation. Such continuation indicates an async call that is not a result of
358+ // an await. So far such calls can only happen as a part of infrastructure helpers/thunks/stubs, as user code must await
359+ // into async code, can't simply call. The context-nutral continuation will run on the current context and will resume
360+ // the next continuation on this same context unconditionally. The idea is to pretend that infrastructure code does not
361+ // exist from the context propagation point of view and the notifier resumes the non-infrastructure code on whatever
362+ // context it wants.
363+ // Basically - we pass through to the notifier the context in which the suspend happened. The notifier needs to know that,
364+ // but may choose to ignore. One example is `IO.Pipelines.Pipe` that can be configured to resume everything on threadpool
365+ // regardless of the calling context.
366+ // It is ok if the leaf continuation runs on a context other that it asked for. It is also possible that non-infra
367+ // continuation gets posted to the original context, but then the context reschedules the action to the
368+ // threadpool (fairly common) or to another context (also possible, but uncommon).
369+ //
370+ // Either way the leaf continuation of the user code can only _ask_ for the context it wants to continue in,
371+ // but may end up running in what the notifier decided, and even then the context/scheduler may decide something else.
372+ // NB: if the leaf user continuation could re-decide and post or schedule itself to another context, the circle of
373+ // re-deciding could never end.
374+ bool isContexNeutralContinuation = ( continuation . Flags & continueFlags ) == 0 ;
352375 try
353376 {
354377 Continuation ? newContinuation = continuation . Resume ( continuation ) ;
@@ -366,7 +389,15 @@ public static unsafe void MoveNext<T, TOps>(T task) where T : Task where TOps :
366389 }
367390 catch ( Exception ex )
368391 {
392+ Continuation ? callerContinuation = continuation . Next ;
369393 Continuation nextContinuation = UnwindToPossibleHandler ( continuation ) ;
394+
395+ if ( nextContinuation != callerContinuation )
396+ {
397+ // If we were unwoud above immediate call into infra thunks, the continuation context takes over.
398+ isContexNeutralContinuation = false ;
399+ }
400+
370401 if ( nextContinuation . Resume == null )
371402 {
372403 // Tail of AsyncTaskMethodBuilderT.SetException
@@ -403,7 +434,8 @@ public static unsafe void MoveNext<T, TOps>(T task) where T : Task where TOps :
403434 return ;
404435 }
405436
406- if ( QueueContinuationFollowUpActionIfNecessary < T , TOps > ( task , continuation ) )
437+ if ( ! isContexNeutralContinuation &&
438+ QueueContinuationFollowUpActionIfNecessary < T , TOps > ( task , continuation ) )
407439 {
408440 contexts . Pop ( ) ;
409441 return ;
@@ -432,6 +464,7 @@ public static void HandleSuspended<T, TOps>(T task, Continuation suspendContinua
432464 CorInfoContinuationFlags . CORINFO_CONTINUATION_CONTINUE_ON_CAPTURED_SYNCHRONIZATION_CONTEXT |
433465 CorInfoContinuationFlags . CORINFO_CONTINUATION_CONTINUE_ON_THREAD_POOL |
434466 CorInfoContinuationFlags . CORINFO_CONTINUATION_CONTINUE_ON_CAPTURED_TASK_SCHEDULER ;
467+
435468 Debug . Assert ( ( headContinuation . Flags & continueFlags ) == 0 ) ;
436469
437470 TOps . SetContinuationState ( task , headContinuation ) ;
0 commit comments