As there is more than one thread available in server-side Blazor applications, it is entirely possible that different components can have code executed on them by various threads.
This is seen most frequently in asynchronous Task
based operations.
For example, multiple components sending an HTTP request to a server will receive individual responses.
Each individual response will resume the calling method using whichever thread the system choses for us
from a pool of available threads.
The easiest way for us to observe this behavior is to create some asynchronous methods that perform an await
.
For this example,
we'll use the OnInitializedAsync
lifecycle method.
To demonstrate this, we'll first need to create a new server-side Blazor application.
Then, in the /Shared folder, create a component named SynchronousInitComponent
.
This component will capture the Thread.ManagedThreadId
of the current thread when OnInitialized
is executed.
This value will then be displayed on the page when our component renders.
<p>Sync rendered by thread @IdOfRenderingThread</p>
@code
{
int IdOfRenderingThread;
protected override void OnInitialized()
{
base.OnInitialized();
IdOfRenderingThread =
System.Threading.Thread.CurrentThread.ManagedThreadId;
}
}
- Line 5
A field is declared to hold a reference to a thread ID. - Line 7
TheOnInitialized
lifecycle method is overridden. - Line 10
The ID of the current thread is stored inIdOfRenderingThread
so it can be rendered. - Line 1
Renders the ID of the thread that was captured on line 10.
Finally, edit the /Pages/Index.razor page to display 5 instances of our new component.
@page "/"
<h1>Components with synchronous OnInitialized()</h1>
@for (int i = 0; i < 5; i++)
{
<SynchronousInitComponent />
}
Running the application will show the same thread ID for each component. Obviously, your thread ID might not be the same as mine.
Components with synchronous OnInitialized()
Sync rendered by thread 4
Sync rendered by thread 4
Sync rendered by thread 4
Sync rendered by thread 4
Sync rendered by thread 4
Asynchronous
Next we'll create another new component in the /Shared folder named AsynchronousInitComponent
.
This component will be identical to the SynchronousInitComponent
,
but will additionally re-assign the value of IdOfRenderingThread
in OnInitializedAsync
after an await
of 1 second.
<p>Async rendered by thread @IdOfRenderingThread</p>
@code
{
int IdOfRenderingThread;
protected override async Task OnInitializedAsync()
{
// Runs synchronously as there is no code in base.OnInitialized(),
// so the same thread is used
await base.OnInitializedAsync().ConfigureAwait(false);
IdOfRenderingThread =
System.Threading.Thread.CurrentThread.ManagedThreadId;
// Awaiting will schedule a job for later, and we will be assigned
// whichever worker thread is next available
await Task.Delay(1000).ConfigureAwait(false);
IdOfRenderingThread =
System.Threading.Thread.CurrentThread.ManagedThreadId;
}
}
- Line 7
TheOnInitializedAsync
lifecycle method is overridden. - Line 12
As with the synchronous component, theManagedThreadId
of the current thread is assigned toIdOfRenderingThread
so it can be rendered by the component. (See note) - Line 17
We allow 1 second to elapse before continuing execution of the method. - Line 18
IdOfRenderingThread
is again updated, showing the ID of the thread that re-rendered the component after theawait
of 1 second on line 17.
Note: It might seem to make sense that the await
on line 11 would run asynchronously.
In fact, it runs synchronously.
This is because the base method does nothing.
There are no awaits to asynchronous code (such as Task.Delay
) so the same thread continues the execution.
We'll also need another page that renders this new component. Create a new page in /Pages named AsyncInitPage.razor with the following mark-up.
@page "/async-init"
<h1>Components with asynchronous OnInitializedAsync()</h1>
@for (int i = 0; i < 5; i++)
{
<AsynchronousInitComponent/>
}
Running the application and navigating to this second page will produce output very similar to the first page, where each component is rendered by a single thread.
Components with asynchronous OnInitializedAsync()
Async rendered by thread 4
Async rendered by thread 4
Async rendered by thread 4
Async rendered by thread 4
Async rendered by thread 4
But then, after 1 second, the await Task.Delay(1000)
in each of the components' OnInitializedAsync
methods will complete
and update the IdOfRenderingThread
before rendering the HTML for the browser.
This time, we can see different threads were used to complete the OnInitializedAsync
methods.
Components with asynchronous OnInitializedAsync()
Async rendered by thread 7
Async rendered by thread 18
Async rendered by thread 10
Async rendered by thread 13
Async rendered by thread 11
What about ConfigureAwait(true)?
Specifying ConfigureAwait(true)
on our await
does not guarantee we will see all of our components rendered on the same
thread that initiated the await
.
SpecifyingConfigureAwait(true)
will still result in a mixture of threads being used for the callbacks.
Components with asynchronous OnInitializedAsync()
Async rendered by thread 11
Async rendered by thread 11
Async rendered by thread 9
Async rendered by thread 13
Async rendered by thread 13
Even if ConfigureAwait(true)
did guarantee we could continue on the same thread,
this still would not ensure our UI is only ever being rendered by a single thread.
A component could be caused to re-render for many reasons, including (but not limited to).
- A callback from a System.Threading.Timer
- An event triggered by another thread on a Singleton instance shared by multiple users
- A data push from another server we've connected to via a Web Socket.
Summary
In server-side Blazor applications there is no single UI thread. Any available thread could be used when rendering work is required.
Additionally, if any method uses an await
on code that performs asynchronous operations,
it is very likely that the thread assigned to continue the processing of
the method will not be the same one that started it.
In a Blazor WebAssembly application (which only has a single thread) there are no threading problems, but in server-side applications, this can cause problems when using a non-thread-safe dependency across multiple components.
This issue will be addressed in the section on OwningComponentBase