Skip to content

Commit a20eea8

Browse files
pavelsavaralewing
authored andcommitted
[WASI] improve single-threaded threadpool (dotnet#107395)
* fix dotnet#104803 * PollWasiEventLoopUntilResolvedVoid * more * wip * CPU-bound work to do * fix exit * Update src/mono/sample/wasi/http-p2/Program.cs Co-authored-by: Larry Ewing <[email protected]> * feedback --------- Co-authored-by: Larry Ewing <[email protected]>
1 parent d42e645 commit a20eea8

File tree

7 files changed

+94
-35
lines changed

7 files changed

+94
-35
lines changed

src/libraries/Common/tests/WasmTestRunner/WasmTestRunner.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public static int Main(string[] args)
2020
return PollWasiEventLoopUntilResolved((Thread)null!, MainAsync(args));
2121

2222
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "PollWasiEventLoopUntilResolved")]
23-
static extern int PollWasiEventLoopUntilResolved(Thread t, Task<int> mainTask);
23+
static extern T PollWasiEventLoopUntilResolved<T>(Thread t, Task<T> mainTask);
2424
}
2525

2626

src/libraries/System.Private.CoreLib/src/System/Threading/Thread.Unix.cs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,16 @@ internal static System.Threading.Tasks.Task RegisterWasiPollableHandle(int handl
1818
return WasiEventLoop.RegisterWasiPollableHandle(handle, cancellationToken);
1919
}
2020

21-
internal static int PollWasiEventLoopUntilResolved(Task<int> mainTask)
21+
internal static T PollWasiEventLoopUntilResolved<T>(Task<T> mainTask)
2222
{
23-
while (!mainTask.IsCompleted)
24-
{
25-
WasiEventLoop.DispatchWasiEventLoop();
26-
}
27-
var exception = mainTask.Exception;
28-
if (exception is not null)
29-
{
30-
throw exception;
31-
}
32-
33-
return mainTask.Result;
23+
return WasiEventLoop.PollWasiEventLoopUntilResolved<T>(mainTask);
3424
}
3525

36-
#endif
26+
internal static void PollWasiEventLoopUntilResolvedVoid(Task mainTask)
27+
{
28+
WasiEventLoop.PollWasiEventLoopUntilResolvedVoid(mainTask);
29+
}
30+
#endif // TARGET_WASI
3731

3832
// the closest analog to Sleep(0) on Unix is sched_yield
3933
internal static void UninterruptibleSleep0() => Thread.Yield();

src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -906,10 +906,7 @@ internal static bool Dispatch()
906906
// thread because it sees a Determining or Scheduled stage, and the current thread is the last thread processing
907907
// work items, the current thread must either see the work item queued by the enqueuer, or it must see a stage of
908908
// Scheduled, and try to dequeue again or request another thread.
909-
#if !TARGET_WASI
910-
// TODO https://github.com/dotnet/runtime/issues/104803
911909
Debug.Assert(workQueue._separated.queueProcessingStage == QueueProcessingStage.Scheduled);
912-
#endif
913910
workQueue._separated.queueProcessingStage = QueueProcessingStage.Determining;
914911
Interlocked.MemoryBarrier();
915912

src/libraries/System.Private.CoreLib/src/System/Threading/Wasi/WasiEventLoop.cs

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Threading.Tasks;
66
using WasiPollWorld.wit.imports.wasi.io.v0_2_1;
77
using Pollable = WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable;
8+
using MonotonicClockInterop = WasiPollWorld.wit.imports.wasi.clocks.v0_2_1.MonotonicClockInterop;
89

910
namespace System.Threading
1011
{
@@ -14,7 +15,9 @@ internal static class WasiEventLoop
1415
// it will be leaked and stay in this list forever.
1516
// it will also keep the Pollable handle alive and prevent it from being disposed
1617
private static readonly List<PollableHolder> s_pollables = new();
17-
private static bool s_tasksCanceled;
18+
private static bool s_checkScheduled;
19+
private static Pollable? s_resolvedPollable;
20+
private static Task? s_mainTask;
1821

1922
internal static Task RegisterWasiPollableHandle(int handle, CancellationToken cancellationToken)
2023
{
@@ -29,18 +32,70 @@ internal static Task RegisterWasiPollable(Pollable pollable, CancellationToken c
2932
// this will register the pollable holder into s_pollables
3033
var holder = new PollableHolder(pollable, cancellationToken);
3134
s_pollables.Add(holder);
35+
36+
ScheduleCheck();
37+
3238
return holder.taskCompletionSource.Task;
3339
}
3440

35-
// this is not thread safe
36-
internal static void DispatchWasiEventLoop()
41+
42+
internal static T PollWasiEventLoopUntilResolved<T>(Task<T> mainTask)
43+
{
44+
try
45+
{
46+
s_mainTask = mainTask;
47+
while (!mainTask.IsCompleted)
48+
{
49+
ThreadPoolWorkQueue.Dispatch();
50+
}
51+
}
52+
finally
53+
{
54+
s_mainTask = null;
55+
}
56+
var exception = mainTask.Exception;
57+
if (exception is not null)
58+
{
59+
throw exception;
60+
}
61+
62+
return mainTask.Result;
63+
}
64+
65+
internal static void PollWasiEventLoopUntilResolvedVoid(Task mainTask)
66+
{
67+
try
68+
{
69+
s_mainTask = mainTask;
70+
while (!mainTask.IsCompleted)
71+
{
72+
ThreadPoolWorkQueue.Dispatch();
73+
}
74+
}
75+
finally
76+
{
77+
s_mainTask = null;
78+
}
79+
80+
var exception = mainTask.Exception;
81+
if (exception is not null)
82+
{
83+
throw exception;
84+
}
85+
}
86+
87+
internal static void ScheduleCheck()
3788
{
38-
ThreadPoolWorkQueue.Dispatch();
39-
if (s_tasksCanceled)
89+
if (!s_checkScheduled && s_pollables.Count > 0)
4090
{
41-
s_tasksCanceled = false;
42-
return;
91+
s_checkScheduled = true;
92+
ThreadPool.UnsafeQueueUserWorkItem(CheckPollables, null);
4393
}
94+
}
95+
96+
internal static void CheckPollables(object? _)
97+
{
98+
s_checkScheduled = false;
4499

45100
var holders = new List<PollableHolder>(s_pollables.Count);
46101
var pending = new List<Pollable>(s_pollables.Count);
@@ -58,13 +113,28 @@ internal static void DispatchWasiEventLoop()
58113

59114
if (pending.Count > 0)
60115
{
116+
var resolvedPollableIndex = -1;
117+
// if there is CPU-bound work to do, we should not block on PollInterop.Poll below
118+
// so we will append pollable resolved in 0ms
119+
// in effect, the PollInterop.Poll would not block us
120+
if (ThreadPool.PendingWorkItemCount > 0 || (s_mainTask != null && s_mainTask.IsCompleted))
121+
{
122+
s_resolvedPollable ??= MonotonicClockInterop.SubscribeDuration(0);
123+
resolvedPollableIndex = pending.Count;
124+
pending.Add(s_resolvedPollable);
125+
}
126+
61127
var readyIndexes = PollInterop.Poll(pending);
62128
for (int i = 0; i < readyIndexes.Length; i++)
63129
{
64130
uint readyIndex = readyIndexes[i];
65-
var holder = holders[(int)readyIndex];
66-
holder.ResolveAndDispose();
131+
if (resolvedPollableIndex != readyIndex)
132+
{
133+
var holder = holders[(int)readyIndex];
134+
holder.ResolveAndDispose();
135+
}
67136
}
137+
68138
for (int i = 0; i < holders.Count; i++)
69139
{
70140
PollableHolder holder = holders[i];
@@ -73,6 +143,8 @@ internal static void DispatchWasiEventLoop()
73143
s_pollables.Add(holder);
74144
}
75145
}
146+
147+
ScheduleCheck();
76148
}
77149
}
78150

@@ -112,19 +184,14 @@ public void ResolveAndDispose()
112184
}
113185

114186
// for GC of abandoned Tasks or for cancellation
115-
private static void CancelAndDispose(object? s)
187+
public static void CancelAndDispose(object? s)
116188
{
117189
PollableHolder self = (PollableHolder)s!;
118190
if (self.isDisposed)
119191
{
120192
return;
121193
}
122194

123-
// Tell event loop to exit early, giving the application a
124-
// chance to quit if the task(s) it is interested in have
125-
// completed.
126-
s_tasksCanceled = true;
127-
128195
// it will be removed from s_pollables on the next run
129196
self.isDisposed = true;
130197
self.pollable.Dispose();

src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Wasi.Mono.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public static partial class ThreadPool
3535
{
3636
// Indicates whether the thread pool should yield the thread from the dispatch loop to the runtime periodically so that
3737
// the runtime may use the thread for processing other work
38-
internal static bool YieldFromDispatchLoop => false;
38+
internal static bool YieldFromDispatchLoop => true;
3939

4040
private const bool IsWorkerTrackingEnabledInConfig = false;
4141

src/mono/sample/wasi/http-p2/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public static class WasiMainWrapper
1313
{
1414
public static async Task<int> MainAsync(string[] args)
1515
{
16+
_ = Task.Delay(100_000_000); // create a task that will not complete before main
1617
await Task.Delay(100);
1718
GC.Collect(); // test that Pollable->Task is not collected until resolved
1819

@@ -78,7 +79,7 @@ public static int Main(string[] args)
7879
return PollWasiEventLoopUntilResolved((Thread)null!, MainAsync(args));
7980

8081
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "PollWasiEventLoopUntilResolved")]
81-
static extern int PollWasiEventLoopUntilResolved(Thread t, Task<int> mainTask);
82+
static extern T PollWasiEventLoopUntilResolved<T>(Thread t, Task<T> mainTask);
8283
}
8384

8485
}

src/mono/wasi/testassets/Http.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,6 @@ public static int Main(string[] args)
8383
return PollWasiEventLoopUntilResolved((Thread)null!, MainAsync(args));
8484

8585
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "PollWasiEventLoopUntilResolved")]
86-
static extern int PollWasiEventLoopUntilResolved(Thread t, Task<int> mainTask);
86+
static extern T PollWasiEventLoopUntilResolved<T>(Thread t, Task<T> mainTask);
8787
}
8888
}

0 commit comments

Comments
 (0)