-
Notifications
You must be signed in to change notification settings - Fork 10.1k
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
[blazor][wasm] Dispatch rendering to main thread #48991
Conversation
src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyDispatcher.cs
Outdated
Show resolved
Hide resolved
6d0e683
to
1ab5597
Compare
1ab5597
to
db1c1c5
Compare
src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyDispatcher.cs
Show resolved
Hide resolved
@mkArtakMSFT could we get this reviewed and merged for the next preview ? |
src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyDispatcher.cs
Show resolved
Hide resolved
self.Send((_) => | ||
{ | ||
try | ||
{ | ||
body(); | ||
} | ||
catch (Exception ex) | ||
{ | ||
exc = ex; | ||
} | ||
}, null); | ||
if (exc != null) | ||
{ | ||
throw exc; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this block? My understanding is that this runs the callback inline, isn't it?
I'm trying to make sense of how this works when you are in a background thread and try to dispatch back to the "main" thread.
My understanding is that the contract for send must block the thread.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this will block the sender thread. If this is the main thread, it would just invoke it.
src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs
Show resolved
Hide resolved
src/Components/WebAssembly/testassets/ThreadingApp/Pages/Counter.razor
Outdated
Show resolved
Hide resolved
# Conflicts: # AspNetCore.sln # src/Components/Components.slnf # src/Components/ComponentsNoDeps.slnf
I think that CI failure is unrelated to this PR. @javiercn are there further questions ? |
} | ||
|
||
_context!.InvokeAsync(workItem); | ||
return Task.CompletedTask; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a mistake, unless I'm confused.
Shouldn't this return the task returned by _context.InvokeAsync
, instead of Task.CompletedTask
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will invoke the void SynchronizationContextExtension.InvokeAsync
and so there is no Task. Also it's blocking synchronous call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More details about the expected behavior in https://github.com/dotnet/aspnetcore/pull/48991/files#r1263731418
_context = SynchronizationContext.Current; | ||
} | ||
|
||
public override bool CheckAccess() => SynchronizationContext.Current == _context || _context == null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain the || _context == null
part of this check?
I would have thought that if SynchronizationContext.Current == null
, then we'd only consider ourselves "having access" if the original _context
was also null. So it seems like the logic would be more correct if the || _context == null
part of this check was removed.
throw exc; | ||
} | ||
return value!; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic around async and exception handling looks strange to me. At least, it will behave very differently from how RendererSynchronizationContext
does.
Dispatcher.InvokeAsync(Action)
- With
WebAssemblyDispatcher
, it blocks the caller until the action is fully complete. If the action throws, then it re-throws instead of returning any task. - With
RendererSynchronizationContextDispatcher
, it doesn't block the caller, and returns aTask
that completes when the action is complete. If the action throws, theTask
is faulted.
- With
Dispatcher.InvokeAsync(Func<Task>)
- With
WebAssemblyDispatcher
, it blocks the caller while the dispatch is in process (up until the recipient yields), and then returns theTask
returned by the recipient. If the recipient throws synchronously, then it re-throws instead of returning any task. - With
RendererSynchronizationContextDispatcher
, it does not block the caller, and returns a aTask
that completes when the dispatch and recipient's Task is complete. If the recipent throws synchronously, the returnedTask
is faulted.
- With
Are these differences in behavior intentional? My guess is we should behave the same as RendererSynchronizationContextDispatcher
/RendererSynchronizationContext
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly please see how RendererSynchronizationContext
deals with unhandled exceptions by calling OnUnhandledException
on the Dispatcher
.
Looks like this PR hasn't been active for some time and the codebase could have been changed in the meantime. |
This PR implements
WebAssemblyDispatcher
which will dispatch the call toComponent.InvokeAsync
back to main thread, where all the rendering/JS interop/networking could happen.Context: with
<WasmEnableThreads>true</WasmEnableThreads>
we could use thread pool in C#.Any async continuations with
.ConfigureAwait(false)
will likely run there.Added new
src/Components/WebAssembly/testassets/ThreadingApp
InvokeAsync
to render it.Contributes to dotnet/runtime#85592
Fixes #48768