-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[blazor][wasm] Dispatch rendering to main thread #48991
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
Changes from all commits
51c7008
6eb9ee8
db1c1c5
4845353
401bf2c
e7c87d5
fe393bf
ffee1e7
068091f
5e56dff
46fd9b7
ae88c7d
2b79ac1
82d97d9
a44a60f
1c7c442
e4e38e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering; | ||
|
|
||
| // When Blazor is deployed with multi-threaded runtime, WebAssemblyDispatcher will help to dispatch all JS interop calls to the main thread. | ||
| // This is necessary because all JS objects have thread affinity. They are only available on the thread (WebWorker) which created them. | ||
| // Also DOM is only available on the main (browser) thread. | ||
| // The calls to InvokeAsync methods are dispatched synchronously and the returned Task is only resolved after the main thread finished the callback. | ||
| internal sealed class WebAssemblyDispatcher : Dispatcher | ||
| { | ||
| public static readonly Dispatcher Instance = new WebAssemblyDispatcher(); | ||
| private readonly SynchronizationContext? _context; | ||
|
|
||
| private WebAssemblyDispatcher() | ||
| { | ||
| // capture the JSSynchronizationContext from the main thread. | ||
| _context = SynchronizationContext.Current; | ||
| } | ||
|
|
||
| public override bool CheckAccess() => SynchronizationContext.Current == _context || _context == null; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you explain the I would have thought that if |
||
|
|
||
| public override Task InvokeAsync(Action workItem) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(workItem); | ||
| if (CheckAccess()) | ||
| { | ||
| workItem(); | ||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| _context!.InvokeAsync(workItem); | ||
| return Task.CompletedTask; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will invoke the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| } | ||
|
|
||
| public override Task InvokeAsync(Func<Task> workItem) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(workItem); | ||
| if (CheckAccess()) | ||
| { | ||
| return workItem(); | ||
| } | ||
|
|
||
| return _context!.InvokeAsync(workItem); | ||
| } | ||
|
|
||
| public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(workItem); | ||
| if (CheckAccess()) | ||
| { | ||
| return Task.FromResult(workItem()); | ||
| } | ||
|
|
||
| return _context!.InvokeAsync(static (workItem) => Task.FromResult(workItem()), workItem); | ||
| } | ||
|
|
||
| public override Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(workItem); | ||
| if (CheckAccess()) | ||
| { | ||
| return workItem(); | ||
| } | ||
|
|
||
| return _context!.InvokeAsync(workItem); | ||
| } | ||
| } | ||
|
|
||
| internal static class SynchronizationContextExtension | ||
| { | ||
| public static void InvokeAsync(this SynchronizationContext self, Action body) | ||
pavelsavara marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| Exception? exc = default; | ||
| self.Send((_) => | ||
| { | ||
| try | ||
| { | ||
| body(); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| exc = ex; | ||
| } | ||
| }, null); | ||
| if (exc != null) | ||
| { | ||
| throw exc; | ||
| } | ||
|
Comment on lines
+75
to
+89
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| } | ||
|
|
||
| public static TRes InvokeAsync<TRes>(this SynchronizationContext self, Func<TRes> body) | ||
| { | ||
| TRes? value = default; | ||
| Exception? exc = default; | ||
| self.Send((_) => | ||
| { | ||
| try | ||
| { | ||
| value = body(); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| exc = ex; | ||
| } | ||
| }, null); | ||
| if (exc != null) | ||
| { | ||
| throw exc; | ||
| } | ||
| return value!; | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Are these differences in behavior intentional? My guess is we should behave the same as
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similarly please see how |
||
|
|
||
| public static TRes InvokeAsync<T1, TRes>(this SynchronizationContext self, Func<T1, TRes> body, T1 p1) | ||
| { | ||
| TRes? value = default; | ||
| Exception? exc = default; | ||
| self.Send((_) => | ||
| { | ||
| try | ||
| { | ||
| value = body(p1); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| exc = ex; | ||
| } | ||
| }, null); | ||
| if (exc != null) | ||
| { | ||
| throw exc; | ||
| } | ||
| return value!; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <Router AppAssembly="@typeof(ThreadingApp.Program).Assembly"> | ||
| <Found Context="routeData"> | ||
| <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> | ||
| <FocusOnNavigate RouteData="@routeData" Selector="h1" /> | ||
| </Found> | ||
| <NotFound> | ||
| <LayoutView Layout="@typeof(MainLayout)"> | ||
| <h2>Not found</h2> | ||
| Sorry, there's nothing at this address. | ||
| </LayoutView> | ||
| </NotFound> | ||
| </Router> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| @page "/counter" | ||
pavelsavara marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| @using System.Runtime.InteropServices | ||
|
|
||
| <h1>Counter</h1> | ||
|
|
||
| <p>Current count: @currentCount</p> | ||
|
|
||
| <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> | ||
|
|
||
| @code { | ||
| int currentCount = 0; | ||
|
|
||
| void IncrementCount() | ||
| { | ||
| currentCount++; | ||
| } | ||
|
|
||
| protected override async Task OnInitializedAsync() | ||
| { | ||
| if(!OperatingSystem.IsBrowser()) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (Thread.CurrentThread.ManagedThreadId != 1) | ||
| { | ||
| throw new Exception("We should be on main thread!"); | ||
| } | ||
|
|
||
| Exception exc = null; | ||
| try | ||
| { | ||
| // send me to the thread pool | ||
| await Task.Delay(10).ConfigureAwait(false); | ||
pavelsavara marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| StateHasChanged(); // render should throw | ||
| } | ||
| catch(Exception ex) | ||
| { | ||
| exc=ex; | ||
| Console.WriteLine(ex.Message); | ||
| } | ||
| if (exc == null || exc.Message != "The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.") | ||
| { | ||
| throw new Exception("We should have thrown here!"); | ||
| } | ||
|
|
||
| // test that we could create new thread | ||
| var tcs = new TaskCompletionSource<int>(); | ||
| var t = new Thread(() => { | ||
| tcs.SetResult(Thread.CurrentThread.ManagedThreadId); | ||
| }); | ||
| t.Start(); | ||
| var newThreadId = await tcs.Task; | ||
| if (newThreadId == 1){ | ||
| throw new Exception("We should be on new thread in the callback!"); | ||
| } | ||
|
|
||
| new Timer(async (state) => | ||
| { | ||
| // send me to the thread pool | ||
| await Task.Delay(10).ConfigureAwait(false); | ||
| if (Thread.CurrentThread.ManagedThreadId == 1) | ||
| { | ||
| throw new Exception("We should be on thread pool thread!"); | ||
| } | ||
|
|
||
| await InvokeAsync(() => | ||
| { | ||
| if (Thread.CurrentThread.ManagedThreadId != 1) | ||
| { | ||
| throw new Exception("We should be on main thread again!"); | ||
| } | ||
| // we are back on main thread | ||
pavelsavara marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| IncrementCount(); | ||
| StateHasChanged(); // render! | ||
| }); | ||
| }, null, 0, 100); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.