Skip to content
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

[browser][MT] InvokeAsync in Blazor WASM components calls into the caller thread instead of the UI thread. #95547

Closed
LostBeard opened this issue Dec 2, 2023 · 5 comments · Fixed by dotnet/aspnetcore#52724
Assignees
Labels
arch-wasm WebAssembly architecture area-VM-threading-mono os-browser Browser variant of arch-wasm
Milestone

Comments

@LostBeard
Copy link

LostBeard commented Dec 2, 2023

Description

ComponentBase.InvokeAsync does not call on the UI thread when called from another thread.

Reproduction Steps

Test project repo: Threads Test

The project msut have <WasmEnableThreads>true</WasmEnableThreads> and reference Microsoft.NET.WebAssembly.Threading. I also have the workload wasm-experimental installed.

In a Blazor WASM threading project replace the Counter.razor page with the below code.

@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current text: @screenData</p>
<button class="btn btn-primary" @onclick="ButtonClicked">Use InvokeAsync</button>

@code {
    private string screenData = "";

    // This does not work using InvokeAsync
    private async void ButtonClicked()
    {
        Console.WriteLine("Message1,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
        screenData = "AfterMessage1";
        StateHasChanged();
        await Task.Run(async () =>
        {
            Console.WriteLine("Message2,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
            await Task.Delay(2000);
            Console.WriteLine("Message3,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
            await InvokeAsync(() =>
            {
                // This is not called on the UI thread as expected
                screenData = "AfterMessage3";
                Console.WriteLine("Message4,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
                // *** Below line throws errors viewable in devtools console ***
                StateHasChanged();
            });
            await Task.Delay(2000);
            Console.WriteLine("Message5,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
        });
        Console.WriteLine("Message6,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
        screenData = "AfterMessage6";
        StateHasChanged();
    }
}

Expected behavior

I expect InvokeAsync (in a Blazor WASM component) to call on the UI thread context.

Actual behavior

InvokeAsync (in a Blazor WASM component) calls on the same thread it is called from.

Regression?

Unknown.

Known Workarounds

This (InvokeAsyncAlt) works in place on InvokeAsync in a Blazor Component

    SynchronizationContext sctx = SynchronizationContext.Current!;
    Task InvokeAsyncAlt(Action action)
    {
        sctx.Post((object state) => action(), null);
        return Task.CompletedTask;
    }
    Task InvokeAsyncAlt(Func<Task> action)
    {
        sctx.Post(async (object state) => await action(), null);
        return Task.CompletedTask;
    }

Configuration

Blazor WebAssembly Standalone template with .Net 8.0.0
Windows 10 x64
.Net 8.0.100
Tested in Chrome 119.0.6045.200 and Firefox 120.0

Other information

I came across this issue while helping answer a question on StackOverflow How do child threads talk to the main (UI) thread in blazor webassembly (multithreading mode)

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Dec 2, 2023
@ghost
Copy link

ghost commented Dec 2, 2023

Tagging subscribers to this area: @mangod9
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

ComponentBase.InvokeAsync does not call on the UI thread when called from another thread.

Reproduction Steps

Test project repo: Threads Test

The project msut have <WasmEnableThreads>true</WasmEnableThreads> and reference Microsoft.NET.WebAssembly.Threading. I also have the workload wasm-experimental installed.

In a Blazor WASM threading project replace the Counter.razor page with the below code.

@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<div>
    Open devtools to see the output and errors if any. Using the InvokeAsync option may cause the page to no longer work until reloaded.
</div>
<p role="status">Current text: @screenData</p>
<button class="btn btn-primary" @onclick="ButtonClicked">Use InvokeAsync</button>
<button class="btn btn-primary" @onclick="ButtonClickedAlt">Use InvokeAsyncAlt</button>

@code {
    private string screenData = "";

    // This does not work using InvokeAsync
    private async void ButtonClicked()
    {
        Console.WriteLine("Message1,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
        screenData = "AfterMessage1";
        StateHasChanged();
        await Task.Run(async () =>
        {
            Console.WriteLine("Message2,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
            await Task.Delay(2000);
            Console.WriteLine("Message3,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
            await InvokeAsync(() =>
            {
                screenData = "AfterMessage3";
                Console.WriteLine("Message4,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
                StateHasChanged();
            });
            await Task.Delay(2000);
            Console.WriteLine("Message5,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
        });
        Console.WriteLine("Message6,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
        screenData = "AfterMessage6";
        StateHasChanged();
    }

    // This works using InvokeAsyncAlt
    private async void ButtonClickedAlt()
    {
        Console.WriteLine("Message1,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
        screenData = "AfterMessage1";
        StateHasChanged();
        await Task.Run(async () =>
        {
            Console.WriteLine("Message2,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
            await Task.Delay(2000);
            Console.WriteLine("Message3,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
            await InvokeAsyncAlt(() =>
            {
                screenData = "AfterMessage3"; 
                Console.WriteLine("Message4,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
                StateHasChanged();
            });

            await Task.Delay(2000);
            Console.WriteLine("Message5,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
        });
        Console.WriteLine("Message6,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
        screenData = "AfterMessage6";
        StateHasChanged();
    }

    SynchronizationContext sctx = SynchronizationContext.Current!;
    Task InvokeAsyncAlt(Action action)
    {
        sctx.Post((object state) => action(), null);
        return Task.CompletedTask;
    }
    Task InvokeAsyncAlt(Func<Task> action)
    {
        sctx.Post(async (object state) => await action(), null);
        return Task.CompletedTask;
    }
}

Expected behavior

I expect InvokeAsync (in a Blazor WASM component) to call on the UI thread context.

Actual behavior

InvokeAsync (in a Blazor WASM component) calls on the same thread it is called from.

Regression?

Unknown.

Known Workarounds

This (InvokeAsyncAlt) works in place on InvokeAsync in a Blazor Component

    SynchronizationContext sctx = SynchronizationContext.Current!;
    Task InvokeAsyncAlt(Action action)
    {
        sctx.Post((object state) => action(), null);
        return Task.CompletedTask;
    }
    Task InvokeAsyncAlt(Func<Task> action)
    {
        sctx.Post(async (object state) => await action(), null);
        return Task.CompletedTask;
    }

Configuration

Blazor WebAssembly Standalone template with .Net 8.0.0
Windows 10 x64
.Net 8.0.100
Tested in Chrome 119.0.6045.200 and Firefox 120.0

Other information

I came across this issue while helping answer a question on StackOverflow How do child threads talk to the main (UI) thread in blazor webassembly (multithreading mode)

Author: LostBeard
Assignees: -
Labels:

area-System.Threading

Milestone: -

@En3Tho
Copy link
Contributor

En3Tho commented Dec 3, 2023

This is similar to how other UI apps are behaving in .Net I believe. Using Task.Run forwards the execution to the threadpool (or well, an analogue of it) where synchronization context is usually null.

I personally solve this by introducing some kind of "UITask" which simply checks if current sync context is the one task expects and if it is, it runs the code right away or does post to sync context.

You can also introduce some kind of awaitable that will do the similar thing like JumpToUiThreadAwaitable

Another question is whether behavior you expect should be on InvokeAsync by default (e.g. auto UI thread switching)

@ghost
Copy link

ghost commented Dec 4, 2023

Tagging subscribers to 'arch-wasm': @lewing
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

ComponentBase.InvokeAsync does not call on the UI thread when called from another thread.

Reproduction Steps

Test project repo: Threads Test

The project msut have <WasmEnableThreads>true</WasmEnableThreads> and reference Microsoft.NET.WebAssembly.Threading. I also have the workload wasm-experimental installed.

In a Blazor WASM threading project replace the Counter.razor page with the below code.

@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current text: @screenData</p>
<button class="btn btn-primary" @onclick="ButtonClicked">Use InvokeAsync</button>

@code {
    private string screenData = "";

    // This does not work using InvokeAsync
    private async void ButtonClicked()
    {
        Console.WriteLine("Message1,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
        screenData = "AfterMessage1";
        StateHasChanged();
        await Task.Run(async () =>
        {
            Console.WriteLine("Message2,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
            await Task.Delay(2000);
            Console.WriteLine("Message3,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
            await InvokeAsync(() =>
            {
                // This is not called on the UI thread as expected
                screenData = "AfterMessage3";
                Console.WriteLine("Message4,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
                // *** Below line throws errors viewable in devtools console ***
                StateHasChanged();
            });
            await Task.Delay(2000);
            Console.WriteLine("Message5,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
        });
        Console.WriteLine("Message6,MyThreadID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
        screenData = "AfterMessage6";
        StateHasChanged();
    }
}

Expected behavior

I expect InvokeAsync (in a Blazor WASM component) to call on the UI thread context.

Actual behavior

InvokeAsync (in a Blazor WASM component) calls on the same thread it is called from.

Regression?

Unknown.

Known Workarounds

This (InvokeAsyncAlt) works in place on InvokeAsync in a Blazor Component

    SynchronizationContext sctx = SynchronizationContext.Current!;
    Task InvokeAsyncAlt(Action action)
    {
        sctx.Post((object state) => action(), null);
        return Task.CompletedTask;
    }
    Task InvokeAsyncAlt(Func<Task> action)
    {
        sctx.Post(async (object state) => await action(), null);
        return Task.CompletedTask;
    }

Configuration

Blazor WebAssembly Standalone template with .Net 8.0.0
Windows 10 x64
.Net 8.0.100
Tested in Chrome 119.0.6045.200 and Firefox 120.0

Other information

I came across this issue while helping answer a question on StackOverflow How do child threads talk to the main (UI) thread in blazor webassembly (multithreading mode)

Author: LostBeard
Assignees: -
Labels:

arch-wasm, untriaged, area-VM-threading-mono

Milestone: -

@lambdageek
Copy link
Member

/cc @pavelsavara

@pavelsavara
Copy link
Member

This is known gap. We didn't finish solving this problem on time to be included in Net8.
My initial attempt was too naive.

@pavelsavara pavelsavara self-assigned this Dec 4, 2023
@pavelsavara pavelsavara added this to the 9.0.0 milestone Dec 4, 2023
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Dec 4, 2023
@pavelsavara pavelsavara added untriaged New issue has not been triaged by the area owner os-browser Browser variant of arch-wasm and removed untriaged New issue has not been triaged by the area owner labels Dec 4, 2023
@pavelsavara pavelsavara changed the title InvokeAsync in Blazor WASM components calls into the caller thread instead of the UI thread. [browser][MT] InvokeAsync in Blazor WASM components calls into the caller thread instead of the UI thread. Jan 9, 2024
@github-actions github-actions bot locked and limited conversation to collaborators Feb 17, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
arch-wasm WebAssembly architecture area-VM-threading-mono os-browser Browser variant of arch-wasm
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants