Skip to content

Commit 96acc3c

Browse files
authored
[RuntimeAsync] Skip over nonuser continuations when searching for awaiter configuration (#121976)
Fixes: #121971 Sometimes we have transparent/infrastructure code between user code and a ValueTask-returning method that wraps IValueTaskSource. An example is an unboxing stub. This is the scenario where we need to figure the configuration of the await operation by looking at continuation that corresponds to the user code (only those are configured). In a case if we see transparent continuations, we should just continue walking continuation chain.
1 parent 9e9eb9b commit 96acc3c

File tree

3 files changed

+89
-3
lines changed

3 files changed

+89
-3
lines changed

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ public static void HandleSuspended<T, TOps>(T task) where T : Task, ITaskComplet
576576
ContinuationFlags.ContinueOnCapturedSynchronizationContext |
577577
ContinuationFlags.ContinueOnThreadPool |
578578
ContinuationFlags.ContinueOnCapturedTaskScheduler;
579+
579580
Debug.Assert((headContinuation.Flags & continueFlags) == 0);
580581

581582
TOps.SetContinuationState(task, headContinuation);
@@ -598,7 +599,7 @@ public static void HandleSuspended<T, TOps>(T task) where T : Task, ITaskComplet
598599
}
599600
else if (vtsNotifier != null)
600601
{
601-
// The awaiter must inform the ValueTaskSource source on whether the continuation
602+
// The awaiter must inform the ValueTaskSource on whether the continuation
602603
// wants to run on a context, although the source may decide to ignore the suggestion.
603604
// Since the behavior of the source takes precedence, we clear the context flags of
604605
// the awaiting continuation (so it will run transparently on what the source decides)
@@ -607,8 +608,18 @@ public static void HandleSuspended<T, TOps>(T task) where T : Task, ITaskComplet
607608
// the continuation chain builds from the innermost frame out and at the time when the
608609
// notifier is created we do not know yet if the caller wants to continue on a context.
609610
ValueTaskSourceOnCompletedFlags configFlags = ValueTaskSourceOnCompletedFlags.None;
610-
ContinuationFlags continuationFlags = headContinuation.Next!.Flags;
611611

612+
// Skip to a nontransparent/user continuation. Such continuaton must exist.
613+
// Since we see a VTS notifier, something was directly or indirectly
614+
// awaiting an async thunk for a ValueTask-returning method.
615+
// That can only happen in nontransparent/user code.
616+
Continuation nextUserContinuation = headContinuation.Next!;
617+
while ((nextUserContinuation.Flags & continueFlags) == 0 && nextUserContinuation.Next != null)
618+
{
619+
nextUserContinuation = nextUserContinuation.Next;
620+
}
621+
622+
ContinuationFlags continuationFlags = nextUserContinuation.Flags;
612623
const ContinuationFlags continueOnContextFlags =
613624
ContinuationFlags.ContinueOnCapturedSynchronizationContext |
614625
ContinuationFlags.ContinueOnCapturedTaskScheduler;
@@ -620,7 +631,7 @@ public static void HandleSuspended<T, TOps>(T task) where T : Task, ITaskComplet
620631
}
621632

622633
// Clear continuation flags, so that continuation runs transparently
623-
headContinuation.Next!.Flags &= ~continueFlags;
634+
nextUserContinuation.Flags &= ~continueFlags;
624635
TOps.ValueTaskSourceOnCompleted(task, vtsNotifier, configFlags);
625636
}
626637
else
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using System.Threading.Tasks.Sources;
8+
using Xunit;
9+
10+
public class ValueTaskSourceAndStubs
11+
{
12+
[Fact]
13+
public static void EntryPoint()
14+
{
15+
SynchronizationContext? original = SynchronizationContext.Current;
16+
SynchronizationContext.SetSynchronizationContext(new MySyncContext());
17+
18+
try
19+
{
20+
new ValueTaskSourceAndStubs().TestAsync(new C()).GetAwaiter().GetResult();
21+
}
22+
finally
23+
{
24+
SynchronizationContext.SetSynchronizationContext(original);
25+
}
26+
}
27+
28+
private async Task TestAsync(IFace i)
29+
{
30+
await i.Foo<string>(0, 1, 2, 3, 4, 5, 6, 7, "value");
31+
}
32+
33+
private struct C : IFace
34+
{
35+
public ValueTask Foo<T>(int a0, int a1, int a2, int a3, int a4, int a5, int a6, int a7, T value)
36+
{
37+
return new ValueTask(new Source(), 0);
38+
}
39+
40+
private class Source : IValueTaskSource
41+
{
42+
public void GetResult(short token)
43+
{
44+
}
45+
46+
public ValueTaskSourceStatus GetStatus(short token)
47+
{
48+
return ValueTaskSourceStatus.Pending;
49+
}
50+
51+
public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
52+
{
53+
Assert.Equal(ValueTaskSourceOnCompletedFlags.UseSchedulingContext, flags);
54+
ThreadPool.UnsafeQueueUserWorkItem(continuation, state, preferLocal: true);
55+
}
56+
}
57+
}
58+
59+
private interface IFace
60+
{
61+
ValueTask Foo<T>(int a0, int a1, int a2, int a3, int a4, int a5, int a6, int a7, T value);
62+
}
63+
64+
private class MySyncContext : SynchronizationContext
65+
{
66+
}
67+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Project Sdk="Microsoft.NET.Sdk.IL">
2+
<PropertyGroup>
3+
<Optimize>True</Optimize>
4+
</PropertyGroup>
5+
<ItemGroup>
6+
<Compile Include="$(MSBuildProjectName).cs" />
7+
</ItemGroup>
8+
</Project>

0 commit comments

Comments
 (0)