-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[wasi] async
/await
on WASIp2
#102894
Comments
Tagging subscribers to this area: @mangod9 |
cc @agocke |
In Mono/Browser when single-threaded, we have thread-less thread pool implemented via emscripten runtime/src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Browser.Mono.cs Lines 82 to 88 in 9fca0c3
runtime/src/mono/mono/utils/mono-threads-wasm.c Lines 348 to 386 in 9fca0c3
runtime/src/mono/browser/runtime/scheduling.ts Lines 47 to 67 in 9fca0c3
runtime/src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Browser.Mono.cs Lines 124 to 136 in 9fca0c3
This has benefit of yielding to the browser event loop. Hope this helps. I would like to learn more on how yielding out of the current (dotnet) component need to look like for WASI p2/p3. |
(Just thinking aloud) The list of "jobs" we have in the thread-less thread pool solves all non-external Do I understand it right that in WASI instead of being called by host on "event" we are expected to have loop in some Meaning that WASI promises will become resolved by the WASI host even without us yielding ? Would that poll loop still work in preview 3 too ? Could How would that look like if we are WASI reactor which exposes async API ? |
To be able to exit that (For context: In browser we marshal JS I'm not sure what would be the answer if there are multiple-threads in WASI. Should we have multiple Does WASI support cancellation ? In preview 3 ? |
Is Is If multiple times, are we going to create |
To simplify the discussion slightly, I don't think there is any strong dependency on multi-threading with the .NET async model. It is possible that you will need to use a different default scheduler in your app model, but I don't know of any requirement that multi-threading is required for .NET async. @stephentoub would probably be the best person to chime in on specific technical decisions that would be recommended for this area. |
For reference, the Discord discussion about this starts here: https://discord.com/channels/732297728826277939/732297825731215521/1245818492322844673 |
(@pavelsavara discussed this on Discord, but I'll answer here also for the record). Yes, WASIp2 requires the guest to run an event loop (maybe in
Yes, the host can make progress on the promises regardless of if or when the guest yields.
Quoting my Discord comments:
Yeah, I was thinking the same thing, but I don't think we need to involve the host with deferred execution at all -- it can all be taken care of in some combination of the .NET runtime and
All exported functions that want to use async/await will need to run an event loop of some kind, but we could make that an implementation detail in |
It's going to be a while before WASI has threads (i.e. post-WASIp3), so I don't think we need to worry about that now. By then, the event loop(s) will be in the host instead of the guest, so the .NET toolchain won't need to deal with it anyway beyond generating thread-safe code.
WASIp2 does not support it, and p3 probably won't either, although I've been doing design work on cancellation with @lukewagner and we plan to support it post-p3. |
No.
It can resolve more than once. I'm not a big fan of that since it makes the host implementation more difficult, but that's how it is :)
Good question. I would say that just because you can use a |
Yeah, I don't think it's intentional, it just so happens that you currently can't use e.g.
I've been reading a lot of his blog posts lately while getting up-to-speed on how |
That's right. The async/await implementation in .NET does need some kind of scheduler that work can be queued to, but it's fine for that scheduler to just be for a single thread pumping in an event loop. I do think you'll want to reroute ThreadPool.QueueUserWorkItem and friends to just schedule to the single event loop. There's a ton of code out there, including but not limited to the async/await infrastructure, that uses ThreadPool directly, or stand-ins for it (like Task.Run), and all of that code should be able to "just work" if those work items are routed to that dispatcher: it just happens it's a pool of size 1. |
Thanks for all the feedback. I'm going to try to summarize what I've read (or inferred) so far:
Please let me know if I've misunderstood or left out anything important. With the above in mind, here's a concrete proposal to drive further discussion:
Note that Thoughts? |
If we keep I think that we will also have to protect it from IL trimming (at least for Mono). For registering external pollable, when would we free that handle ? I'm also missing the part in which dotnet Task is returned/exported as |
Yes; I've been doing the equivalent in Rust and Python, and that's what I'm doing here: https://github.com/dicej/dotnet9-wasi-http-example/blob/da7541017f944247e7614a3cad2f663508118837/PollTaskScheduler.cs#L45-L52 -- i.e. for each ready
I assume you can call Same story for p3 promises -- the host will notify the guest when each one resolves, which will trigger a
The flow of control would look something like this:
See https://github.com/dicej/dotnet9-wasi-http-example/blob/snapshot/PollTaskScheduler.cs for details. Does that help? |
Sorry, my bad. Pollable will keep
What I mean to ask is that we have export We can pass pollable as result or as parameter of a function. And you can do it on imports and exports. I'm asking about the other direction.
I'm not sure that you need your own |
Ah, I see what you're saying. For WASIp2, there's no concept of guest-created For WASIp3, there will be a new Component Model ABI for async exports, and in that case the guest can and should return to the host if the
There's no need for a
Yes, I think you're right. I just referred to that because I expect |
The plan looks good to me. Let's try it in NativeAOT LLVM branch. And we could catch up with it in Mono later. @yowl @SingleAccretion do you have any comments ? |
My primary concern is layering, i. e. the question of who provides the event loop implementation ( For reference, the interface between |
That works for me.
I think that should be fine, yes, although I'm having trouble finding a If someone can explain to me the proper way for a hypothetical |
Actually, I think a better interface for WASI would be something like |
Yep. It was just an example function name.
It doesn't really matter a whole lot what the private interface looks like mechanically. For example, it can be a private static method The main concern is that the interface be small. The thread pool's queue internal machinery is pretty involved. It seems unlikely that we will be able to fit through something like |
Sounds good. How would the |
Is the |
No. Finalization should work for fully synchronous applications. |
Another question about the division of responsibility between the .NET runtime and I'm currently working on adding a The first thing that comes to mind would be to have the WASI implementation of I do expect we'll need to bundle As usual, I'm mostly thinking aloud here and am open to whatever the experts here think is best. |
I probably don't fully follow. Testing would also require runtime in all cases, right ?
I thought that you are going to use And I though that this generated code would contain the generated implementation of the And if not, perhaps runtime could have internal interface IWasiEventLoop
{
void PollAllPollables();
Task RegisterPollable(Pollable pollable);
} And static member on the WASI partial class ThreadPool
{
private static IWasiEventLoop? WasiEventLoop;
internal static void SetEventLoopQueueFunction(IWasiEventLoop wasiEventLoop)
} If we don't want to include Task RegisterPollable(object pollable) Or even just the handle value Task RegisterPollable(uint pollableHandle) |
Well extra internal interface type |
Yes, that's the plan. I'm writing
We were leaning towards putting reusable code like |
Eventually that code belongs to runtime repo, I think. I still don't understand the preference to have it as part of wit-bindgen. @SingleAccretion could you please elaborate ? |
As I said above:
The event loop must be generated at every entry point. The only entry point the runtime controls is I therefore see Additionally, This view is not set in stone. If we can come up with a layering that makes more sense given the constraints we have, let's make it happen. Let's refocus this |
Perhaps there's a middle ground:
There's a precedent for having
|
That is what I was referring to - unsafe accessors are (fast) private reflection. Unless we decide to consume This is again mostly driven by the compatibility guarantees - once a public API is there in the runtime, it is there forever. If
This is the sort of model I was thinking about. Since we know |
It certainly makes sense for a NuGet package that uses Anyway, it sounds like the important point here is that we don't want to make any temporary APIs public in this repo, and we can be flexible about where each bit of code lives as long as we stick to that rule. |
Thanks! |
I realized that some C# programs don't need to use HTTP and so the whole thing could be trimmed.
We always do async main in the browser for yielding reasons. I guess the similar reasons will come to WASI with p3. If there is any import which uses pollable, it also means that there must be async main or async export. So, maybe always async main would simplify the WASI design too ? |
Most of it could be trimmed, yes. But at minimum we'll still need a way for Speaking of trimming: how can I tell the compiler not to trim an |
You can add You can also add xml config (but C# code is preferable). See
Also this seems related #101195 |
Normally nothing special would be needed since trimming is performed on the whole final app, and it understands |
Right.
Very possible. It depends on what kind of app models WASI would be primarily targeting - library-like or application-like. We can't make library-likes work without code generation à la |
I would say WASI (and the Component Model on which it is based) is primarily targeting library-like things going forward. So I would say there's no urgent need for built-in async main support in the runtime; |
Here's what I have so far: dotnet/runtimelab#2614 Note that I'm targeting the I've run some high-level, manual integration tests using https://github.com/dicej/dotnet9-wasi-http-example/tree/snapshot, and things are looking good there. Now I'm going to look at adding and/or reusing tests in the runtime repo and add the result to that PR. |
Question: what should If I'm reading the code correctly, it looks like it's unsupported on the browser when multi-threading is disabled, so maybe we do the same for WASI? https://github.com/dotnet/runtimelab/blob/53342bee4b89a6c066d6eb3c7549028ca479c451/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs#L394-L397 |
Yes, I think we need the same for single-threaded WASI. |
The first pass of trimming is done (without final app) on the individual runtime assemblies separately. There is already ton of descriptors dealing with it in shared and in mono |
this is done |
I've been working to support
async
/await
in C# for doing concurrent I/O when targeting WASIp2. A key piece of that is thewasi:io/poll#poll
function, which accepts an arbitrary number ofpollable
handles representing e.g. socket readiness, HTTP protocol events, timer events, etc. It's analogous to the traditional POSIXpoll(2)
function. My goal is to provide aSystem.Threading.Tasks
-based abstraction on top ofwasi:io/poll
that supports idiomatic use ofasync
/await
, including the variousTask
combinators such asWhenAny
,WhenAll
,WhenEach
. I've done similar work for Python (using a customasyncio
event loop) and Rust (using a customasync
runtime), and am hoping to do the same for .NET.So far, I've managed to write a custom TaskScheduler which supports simple cases by maintaining a list of
Task
s and a list of thePollable
s those tasks areawait
ing. It has aRun
method which, in a loop, alternates between theTask
list and thePollable
list, executing the former and callingwasi:io/poll#poll
on the latter. That works well for simple cases.However, I've had trouble building more sophisticated examples using e.g. the new
Task.WhenEach
combinator due to the somewhat pervasive use ofThreadPool
as a deferred execution mechanism throughout theSystem.Threading.Tasks
library code. Given that WASI does not support multithreading and won't for the foreseeable future,ThreadPool
's methods currently throwSystem.PlatformNotSupportedException
, which makes it something of a landmine. For example, even though there's nothing inherently multithreaded aboutTask.WhenEach
, the current implementation relies on aManualResetValueTaskSourceCore
withRunContinuationsAsynchronously
set to true, which means it always queues continuations usingThreadPool.UnsafeQueueUserWorkItem
.Given that significant pieces of .NET's
async
/await
infrastructure currently relies on multithreading to function, I'm wondering what our options are for WASI. A few come to mind:async
/await
and use callbacks for concurrency in WASIp2. Besides being un-ergonomic, it would significantly restrict the set of both standard and third-party library features available for use on that platform, given that anything that deals with I/O is presumably either synchronous and multithreaded or asynchronous based onasync
/await
.async
/await
, but disallow use of features such asTask.WhenEach
which requireThreadPool
as an implementation detail, and possibly provide alternative implementations of those features which are single-thread-friendly.System.Threading.Tasks
which are not inherently multithreaded (but currently useThreadPool
) to support an alternative, single-threaded deferred execution mechanism on platforms that do not support multithreading.ThreadPool
implementation which simply queues work items without executing them, and provide some public API for running them from the main (and only) thread, e.g. as part of an application event loop.Thoughts?
I should note that I'm quite new to the .NET ecosystem, so I'm happy to be corrected if I've misunderstood anything.
See also #98957, for which I would consider this issue a prerequisite.
The text was updated successfully, but these errors were encountered: