Skip to content

Commit d46c447

Browse files
authored
Support moving continuations to SynchronizationContext/TaskScheduler (#117314)
This adds support for moving continuations after task awaits to the right context: * For unconfigured task awaits or `ConfigureAwait(true)`, continuations are * posted to the captured `SynchronizationContext` or `TaskScheduler` by the BCL * For `ConfigureAwait(false)` continuations are moved to the thread pool The JIT inserts a call to a new `AsyncHelpers.CaptureContinuationContext` when suspending because of a task await. That function either captures the current `SynchronizationContext` or `TaskScheduler` into the continuation in a known place. Later the BCL will fetch the context from the continuation object based on a flag to determine whether the continuation needs to be posted somewhere else or whether it can be inlined. There is a new `Task`-derived `ThunkTask` that replaces the existing C#-async-function-with-a-loop and which handles the continuations explicitly. Inlining continuations on the same thread currently has no check on stack depth. This might be necessary to add. There are some differences compared to async 1 with how continuations are executed. In particular some common cases that resulted in unbounded stack usage with async 1 does _not_ result in increasing stack usage with runtime async. Hence I am not 100% sure whether we need a mitigation or not, but more investigation is needed from my side. I'd like to leave that as a follow-up. To get to the right behavior this PR disables code inlining of task-awaited functions. This is a large concession, but getting the behavior right in the face of inlining proved to be quite tricky with the JIT's current internal representation. Work to reenable this will be done separately. There is still one missing piece for correctness: saving and restoring the `Thread._synchronizationContext` field around async calls. It runs into similar trickiness when trying to represent the expected behavior (which is that the field is restored around synchronous calls, but _not_ restored on resumption).
1 parent b58e37d commit d46c447

File tree

13 files changed

+583
-171
lines changed

13 files changed

+583
-171
lines changed

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs

Lines changed: 350 additions & 152 deletions
Large diffs are not rendered by default.

src/coreclr/inc/corinfo.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1712,6 +1712,15 @@ enum CorInfoContinuationFlags
17121712
// OSR method saved in the beginning of 'Data', or -1 if the continuation
17131713
// belongs to a tier 0 method.
17141714
CORINFO_CONTINUATION_OSR_IL_OFFSET_IN_DATA = 4,
1715+
// If this bit is set the continuation should continue on the thread
1716+
// pool.
1717+
CORINFO_CONTINUATION_CONTINUE_ON_THREAD_POOL = 8,
1718+
// If this bit is set the continuation has a SynchronizationContext
1719+
// that we should continue on.
1720+
CORINFO_CONTINUATION_CONTINUE_ON_CAPTURED_SYNCHRONIZATION_CONTEXT = 16,
1721+
// If this bit is set the continuation has a TaskScheduler
1722+
// that we should continue on.
1723+
CORINFO_CONTINUATION_CONTINUE_ON_CAPTURED_TASK_SCHEDULER = 32,
17151724
};
17161725

17171726
struct CORINFO_ASYNC_INFO
@@ -1737,6 +1746,7 @@ struct CORINFO_ASYNC_INFO
17371746
CORINFO_METHOD_HANDLE captureExecutionContextMethHnd;
17381747
// Method handle for AsyncHelpers.RestoreExecutionContext
17391748
CORINFO_METHOD_HANDLE restoreExecutionContextMethHnd;
1749+
CORINFO_METHOD_HANDLE captureContinuationContextMethHnd;
17401750
};
17411751

17421752
// Flags passed from JIT to runtime.

src/coreclr/jit/async.cpp

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,13 @@ ContinuationLayout AsyncTransformation::LayOutContinuation(BasicBlock*
10401040
block->getTryIndex(), layout.ExceptionGCDataIndex);
10411041
}
10421042

1043+
if (call->GetAsyncInfo().ContinuationContextHandling == ContinuationContextHandling::ContinueOnCapturedContext)
1044+
{
1045+
layout.ContinuationContextGCDataIndex = layout.GCRefsCount++;
1046+
JITDUMP(" Continuation continues on captured context; context will be at GC@+%02u in GC data\n",
1047+
layout.ContinuationContextGCDataIndex);
1048+
}
1049+
10431050
if (call->GetAsyncInfo().ExecutionContextHandling == ExecutionContextHandling::AsyncSaveAndRestore)
10441051
{
10451052
layout.ExecContextGCDataIndex = layout.GCRefsCount++;
@@ -1200,13 +1207,16 @@ BasicBlock* AsyncTransformation::CreateSuspension(
12001207
LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_comp, storeState));
12011208

12021209
// Fill in 'flags'
1203-
unsigned continuationFlags = 0;
1210+
const AsyncCallInfo& callInfo = call->GetAsyncInfo();
1211+
unsigned continuationFlags = 0;
12041212
if (layout.ReturnInGCData)
12051213
continuationFlags |= CORINFO_CONTINUATION_RESULT_IN_GCDATA;
12061214
if (block->hasTryIndex())
12071215
continuationFlags |= CORINFO_CONTINUATION_NEEDS_EXCEPTION;
12081216
if (m_comp->doesMethodHavePatchpoints() || m_comp->opts.IsOSR())
12091217
continuationFlags |= CORINFO_CONTINUATION_OSR_IL_OFFSET_IN_DATA;
1218+
if (callInfo.ContinuationContextHandling == ContinuationContextHandling::ContinueOnThreadPool)
1219+
continuationFlags |= CORINFO_CONTINUATION_CONTINUE_ON_THREAD_POOL;
12101220

12111221
newContinuation = m_comp->gtNewLclvNode(m_newContinuationVar, TYP_REF);
12121222
unsigned flagsOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationFlagsFldHnd);
@@ -1386,6 +1396,51 @@ void AsyncTransformation::FillInGCPointersOnSuspension(const ContinuationLayout&
13861396
}
13871397
}
13881398

1399+
if (layout.ContinuationContextGCDataIndex != UINT_MAX)
1400+
{
1401+
// Insert call AsyncHelpers.CaptureContinuationContext(ref
1402+
// newContinuation.GCData[ContinuationContextGCDataIndex], ref newContinuation.Flags).
1403+
GenTree* contextElementPlaceholder = m_comp->gtNewZeroConNode(TYP_BYREF);
1404+
GenTree* flagsPlaceholder = m_comp->gtNewZeroConNode(TYP_BYREF);
1405+
GenTreeCall* captureCall =
1406+
m_comp->gtNewCallNode(CT_USER_FUNC, m_asyncInfo->captureContinuationContextMethHnd, TYP_VOID);
1407+
1408+
captureCall->gtArgs.PushFront(m_comp, NewCallArg::Primitive(flagsPlaceholder));
1409+
captureCall->gtArgs.PushFront(m_comp, NewCallArg::Primitive(contextElementPlaceholder));
1410+
1411+
m_comp->compCurBB = suspendBB;
1412+
m_comp->fgMorphTree(captureCall);
1413+
1414+
LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_comp, captureCall));
1415+
1416+
// Now replace contextElementPlaceholder with actual address of the context element
1417+
LIR::Use use;
1418+
bool gotUse = LIR::AsRange(suspendBB).TryGetUse(contextElementPlaceholder, &use);
1419+
assert(gotUse);
1420+
1421+
GenTree* objectArr = m_comp->gtNewLclvNode(objectArrLclNum, TYP_REF);
1422+
unsigned offset = OFFSETOF__CORINFO_Array__data + (layout.ContinuationContextGCDataIndex * TARGET_POINTER_SIZE);
1423+
GenTree* contextElementOffset =
1424+
m_comp->gtNewOperNode(GT_ADD, TYP_BYREF, objectArr, m_comp->gtNewIconNode((ssize_t)offset, TYP_I_IMPL));
1425+
1426+
LIR::AsRange(suspendBB).InsertBefore(contextElementPlaceholder, LIR::SeqTree(m_comp, contextElementOffset));
1427+
use.ReplaceWith(contextElementOffset);
1428+
LIR::AsRange(suspendBB).Remove(contextElementPlaceholder);
1429+
1430+
// And now replace flagsPlaceholder with actual address of the flags
1431+
gotUse = LIR::AsRange(suspendBB).TryGetUse(flagsPlaceholder, &use);
1432+
assert(gotUse);
1433+
1434+
newContinuation = m_comp->gtNewLclvNode(m_newContinuationVar, TYP_REF);
1435+
unsigned flagsOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationFlagsFldHnd);
1436+
GenTree* flagsOffsetNode = m_comp->gtNewOperNode(GT_ADD, TYP_BYREF, newContinuation,
1437+
m_comp->gtNewIconNode((ssize_t)flagsOffset, TYP_I_IMPL));
1438+
1439+
LIR::AsRange(suspendBB).InsertBefore(flagsPlaceholder, LIR::SeqTree(m_comp, flagsOffsetNode));
1440+
use.ReplaceWith(flagsOffsetNode);
1441+
LIR::AsRange(suspendBB).Remove(flagsPlaceholder);
1442+
}
1443+
13891444
if (layout.ExecContextGCDataIndex != UINT_MAX)
13901445
{
13911446
GenTreeCall* captureExecContext =

src/coreclr/jit/async.h

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ struct LiveLocalInfo
1818

1919
struct ContinuationLayout
2020
{
21-
unsigned DataSize = 0;
22-
unsigned GCRefsCount = 0;
23-
ClassLayout* ReturnStructLayout = nullptr;
24-
unsigned ReturnSize = 0;
25-
bool ReturnInGCData = false;
26-
unsigned ReturnValDataOffset = UINT_MAX;
27-
unsigned ExceptionGCDataIndex = UINT_MAX;
28-
unsigned ExecContextGCDataIndex = UINT_MAX;
21+
unsigned DataSize = 0;
22+
unsigned GCRefsCount = 0;
23+
ClassLayout* ReturnStructLayout = nullptr;
24+
unsigned ReturnSize = 0;
25+
bool ReturnInGCData = false;
26+
unsigned ReturnValDataOffset = UINT_MAX;
27+
unsigned ExceptionGCDataIndex = UINT_MAX;
28+
unsigned ExecContextGCDataIndex = UINT_MAX;
29+
unsigned ContinuationContextGCDataIndex = UINT_MAX;
2930
const jitstd::vector<LiveLocalInfo>& Locals;
3031

3132
explicit ContinuationLayout(const jitstd::vector<LiveLocalInfo>& locals)

src/coreclr/jit/compiler.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4430,6 +4430,7 @@ class Compiler
44304430
#endif
44314431
// This call is a task await
44324432
PREFIX_IS_TASK_AWAIT = 0x00000080,
4433+
PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT = 0x00000100,
44334434
};
44344435

44354436
static void impValidateMemoryAccessOpcode(const BYTE* codeAddr, const BYTE* codeEndp, bool volatilePrefix);

src/coreclr/jit/gentree.h

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4328,10 +4328,21 @@ enum class ExecutionContextHandling
43284328
AsyncSaveAndRestore,
43294329
};
43304330

4331+
enum class ContinuationContextHandling
4332+
{
4333+
// No special handling of SynchronizationContext/TaskScheduler is required.
4334+
None,
4335+
// Continue on SynchronizationContext/TaskScheduler
4336+
ContinueOnCapturedContext,
4337+
// Continue on thread pool thread
4338+
ContinueOnThreadPool,
4339+
};
4340+
43314341
// Additional async call info.
43324342
struct AsyncCallInfo
43334343
{
4334-
ExecutionContextHandling ExecutionContextHandling = ExecutionContextHandling::None;
4344+
ExecutionContextHandling ExecutionContextHandling = ExecutionContextHandling::None;
4345+
ContinuationContextHandling ContinuationContextHandling = ContinuationContextHandling::None;
43354346
};
43364347

43374348
// Return type descriptor of a GT_CALL node.

src/coreclr/jit/importer.cpp

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9094,17 +9094,18 @@ void Compiler::impImportBlockCode(BasicBlock* block)
90949094
// many other places. We unfortunately embed that knowledge here.
90959095
if (opcode != CEE_CALLI)
90969096
{
9097-
bool isAwait = false;
9098-
// TODO: The configVal should be wired to the actual implementation
9099-
// that control the flow of sync context.
9100-
// We do not have that yet.
9101-
int configVal = -1; // -1 not configured, 0/1 configured to false/true
9097+
bool isAwait = false;
9098+
int configVal = -1; // -1 not configured, 0/1 configured to false/true
91029099
if (compIsAsync() && JitConfig.JitOptimizeAwait())
91039100
{
91049101
if (impMatchTaskAwaitPattern(codeAddr, codeEndp, &configVal))
91059102
{
91069103
isAwait = true;
91079104
prefixFlags |= PREFIX_IS_TASK_AWAIT;
9105+
if (configVal != 0)
9106+
{
9107+
prefixFlags |= PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT;
9108+
}
91089109
}
91099110
}
91109111

src/coreclr/jit/importercalls.cpp

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -701,17 +701,26 @@ var_types Compiler::impImportCall(OPCODE opcode,
701701
{
702702
AsyncCallInfo asyncInfo;
703703

704-
JITDUMP("Call is an async ");
705-
706704
if ((prefixFlags & PREFIX_IS_TASK_AWAIT) != 0)
707705
{
708-
JITDUMP("task await\n");
706+
JITDUMP("Call is an async task await\n");
709707

710708
asyncInfo.ExecutionContextHandling = ExecutionContextHandling::SaveAndRestore;
709+
710+
if ((prefixFlags & PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT) != 0)
711+
{
712+
asyncInfo.ContinuationContextHandling = ContinuationContextHandling::ContinueOnCapturedContext;
713+
JITDUMP(" Continuation continues on captured context\n");
714+
}
715+
else
716+
{
717+
asyncInfo.ContinuationContextHandling = ContinuationContextHandling::ContinueOnThreadPool;
718+
JITDUMP(" Continuation continues on thread pool\n");
719+
}
711720
}
712721
else
713722
{
714-
JITDUMP("non-task await\n");
723+
JITDUMP("Call is an async non-task await\n");
715724
// Only expected non-task await to see in IL is one of the AsyncHelpers.AwaitAwaiter variants.
716725
// These are awaits of custom awaitables, and they come with the behavior that the execution context
717726
// is captured and restored on suspension/resumption.
@@ -7884,6 +7893,14 @@ void Compiler::impMarkInlineCandidateHelper(GenTreeCall* call,
78847893
return;
78857894
}
78867895

7896+
if (call->IsAsync() && (call->GetAsyncInfo().ContinuationContextHandling != ContinuationContextHandling::None))
7897+
{
7898+
// Cannot currently handle moving to captured context/thread pool when logically returning from inlinee.
7899+
//
7900+
inlineResult->NoteFatal(InlineObservation::CALLSITE_CONTINUATION_HANDLING);
7901+
return;
7902+
}
7903+
78877904
// Ignore indirect calls, unless they are indirect virtual stub calls with profile info.
78887905
//
78897906
if (call->gtCallType == CT_INDIRECT)

src/coreclr/jit/inline.def

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ INLINE_OBSERVATION(RETURN_TYPE_MISMATCH, bool, "return type mismatch",
163163
INLINE_OBSERVATION(STFLD_NEEDS_HELPER, bool, "stfld needs helper", FATAL, CALLSITE)
164164
INLINE_OBSERVATION(TOO_MANY_LOCALS, bool, "too many locals", FATAL, CALLSITE)
165165
INLINE_OBSERVATION(PINVOKE_EH, bool, "PInvoke call site with EH", FATAL, CALLSITE)
166+
INLINE_OBSERVATION(CONTINUATION_HANDLING, bool, "Callsite needs continuation handling", FATAL, CALLSITE)
166167

167168
// ------ Call Site Performance -------
168169

src/coreclr/vm/corelib.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,7 @@ DEFINE_METHOD(ASYNC_HELPERS, FINALIZE_VALUETASK_RETURNING_THUNK_1, Finalize
728728
DEFINE_METHOD(ASYNC_HELPERS, UNSAFE_AWAIT_AWAITER_1, UnsafeAwaitAwaiter, GM_T_RetVoid)
729729
DEFINE_METHOD(ASYNC_HELPERS, CAPTURE_EXECUTION_CONTEXT, CaptureExecutionContext, NoSig)
730730
DEFINE_METHOD(ASYNC_HELPERS, RESTORE_EXECUTION_CONTEXT, RestoreExecutionContext, NoSig)
731+
DEFINE_METHOD(ASYNC_HELPERS, CAPTURE_CONTINUATION_CONTEXT, CaptureContinuationContext, NoSig)
731732

732733
DEFINE_CLASS(SPAN_HELPERS, System, SpanHelpers)
733734
DEFINE_METHOD(SPAN_HELPERS, MEMSET, Fill, SM_RefByte_Byte_UIntPtr_RetVoid)

0 commit comments

Comments
 (0)