英文:
Best practice to run synchronous and asynchronous work on button click in Blazor
问题
让我们假设我想在按钮点击时运行两个工作任务,它们都需要一些时间。第一个任务不是线程安全的,因此无法将其设置为异步。第二个任务是异步的。我希望用户界面立即确认按钮已被点击,并且例如禁用它。除了在isBusy = true;
之后迅速加入 await Task.Yield();
,是否有更好的方法?或者这是唯一的方法?
@using System.Threading;
<MudButton OnClick="DoSomethingAsync" Disabled="@isBusy">Do something</MudButton>
@code {
bool isBusy;
async Task DoSomethingAsync()
{
isBusy = true;
Thread.Sleep(2000); // 模拟同步工作
await Task.Delay(2000); // 模拟异步工作
isBusy = false;
}
}
我知道理想情况下,所有长时间运行的操作都应该是异步的,不会阻塞用户界面线程。但如果我有一个无法在主线程之外运行并且无法转换为异步任务的同步操作,我想知道的是有效的方法。
英文:
Let's assume that I want to run two bits of work on button click and they both take some time. First bit is not thread-safe, so it is not feasible to make it async. Second bit is awaited. I want UI to acknowledge right away that the button was clicked and for instance disable it. Is there a better approach than slamming await Task.Yield();
after isBusy = true;
, or this is the way?
@using System.Threading;
<MudButton OnClick="DoSomethingAsync" Disabled="@isBusy">Do something</MudButton>
@code {
bool isBusy;
async Task DoSomethingAsync()
{
isBusy = true;
Thread.Sleep(2000); // simulate sync work
await Task.Delay(2000); // simulate async work
isBusy = false;
}
}
https://try.mudblazor.com/snippet/waGHOLGOHpPUkLLM
I know that ideally, all long-running operations would be asynchronous and wouldn't block the UI thread. But I want to know valid approach, if I have a synchronous operation that can't be run off the main thread and can't be converted to asynchronous task.
答案1
得分: 1
我希望UI立刻确认按钮已被点击,例如禁用它。是否有比在isBusy = true;之后立即执行await Task.Yield();更好的方法?
我更倾向于使用Task.Delay(1)
而不是Task.Yield()
,但除此之外,这是在WebAssembly上实现您想要的最佳/唯一方法。
在Blazor Server上,您可以在另一个线程上执行阻塞代码:
await Task.Run(() => { Thread.Sleep(2000); });
我仍然建议在设置isBusy之后放置一个Delay(1),但也许没有那个也可以工作。
英文:
> I want UI to acknowledge right away that the button was clicked and for instance disable it. Is there a better approach than slamming await Task.Yield(); after isBusy = true;,
I prefer Task.Delay(1)
over Task.Yield()
but for the rest this is the best/only way to do what you want on WebAssembly.
On Blazor Server you can execute the blocking code on another Thread:
await task.Run(() => { Thread.Sleep(2000); });
I would still put a Delay(1) after setting isBusy but it might work without that.
答案2
得分: 0
在isBusy = true;
之后使用await Task.Yield();
是否有更好的方法,或者这种方法就是唯一的方法?
你并没有强制执行任何操作。您设置了第一个 yield 发生的地方,从而产生了中间的“我忙”的渲染。
这是对发生情况的详细解释。
一些背景信息:
-
所有 Blazor UI 代码(生命周期方法、UI 事件和回调)都在一个同步上下文 [SC] 上执行,它强制执行单一的逻辑执行线程。
-
在
ComponentBase
继承的组件中,UI 事件由注册的IHandleEvent.HandleEventAsync
处理。以下是一个简化版本:
async Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem item, object? obj)
{
var uiTask = item.InvokeAsync(obj);
var isCompleted = uiTask.IsCompleted || uiTask.IsCanceled;
if (!isCompleted)
{
this.StateHasChanged();
await uiTask;
}
this.StateHasChanged();
}
当按钮被点击并且渲染器处理事件时。
会调用 IHandleEvent.HandleEventAsync
并传递注册的处理程序 [DoSomethingAsync]。
它调用了 DoSomethingAsync
:
var uiTask = item.InvokeAsync(obj);
DoSomethingAsync
的前两行是一个同步块,因此会运行并阻塞 SC。
isBusy = true;
Thread.Sleep(2000); // 模拟同步工作
在 2000 毫秒后,我们达到了 await:
await Task.Delay(2000); // 模拟异步工作
这在 SC 上创建了一个继续,并产生了中断。
现在在 IHandleEvent.HandleEventAsync
中运行此代码块。它会将渲染请求排队到渲染器的队列上并进行中断(如果 uiTask
还未完成)。渲染器获得对 SC 的控制权,处理其队列并渲染组件。
var isCompleted = uiTask.IsCompleted || uiTask.IsCanceled;
if (!isCompleted)
{
this.StateHasChanged();
await uiTask;
}
在 2000 毫秒后:
await Task.Delay(2000); // 模拟异步工作
完成并且继续运行。
isBusy = false;
在 IHandleEvent.HandleEventAsync
中,uiTask
现在已经完成,因此:
this.StateHasChanged();
运行并排队渲染。SC 现在空闲,因此渲染器处理其队列并渲染组件。
如果您正在运行 Blazor Server,则可以将同步任务卸载到线程池。如果不是,则只能使用 SC。
英文:
> Is there a better approach than slamming await Task.Yield();
after isBusy = true;
, or this is the way?
You aren't Slamming anything. You're setting where the first yield happens, and thus the intermediate "I'm Busy" render occurs.
Here's a detailed explanation of what's happening.
Some background information:
-
All BLazor UI code [lifecycle methods, UI events an callbacks] is executed on a Synchronisation Context [SC from now on], which enforces a single logical thread of execution.
-
In
ComponentBase
inherited components, UI events are handled by the registeredIHandleEvent.HandleEventAsync
. This is a simplified version.
async Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem item, object? obj)
{
var uiTask = item.InvokeAsync(obj);
var isCompleted = uiTask.IsCompleted || uiTask.IsCanceled;
if (!isCompleted)
{
this.StateHasChanged();
await uiTask;
}
this.StateHasChanged();
}
When the button gets clicked and the renderer handles the event.
IHandleEvent.HandleEventAsync
is invoked and passed the registered handler [DoSomethingAsync].
It invokes DoSomethingAsync
var uiTask = item.InvokeAsync(obj);
The first two lines of DoSomethingAsync
are a synchronous block so run and block the SC.
isBusy = true;
Thread.Sleep(2000); // simulate sync work
After 2000 ms, we reach the await:
await Task.Delay(2000); // simulate async work
This creates a continuation on the SC, and yields.
This code block in IHandleEvent.HandleEventAsync
now runs. It queues a render request on the Renderer's queue and yields [if uiTask han't completed]. The Renderer gets control of the SC, services it's queue and renders the component.
var isCompleted = uiTask.IsCompleted || uiTask.IsCanceled;
if (!isCompleted)
{
this.StateHasChanged();
await uiTask;
After 2000 ms:
await Task.Delay(2000); // simulate async work
completes and the contination runs to completion.
isBusy = false;
In IHandleEvent.HandleEventAsync
uiTask
is now completes so:
this.StateHasChanged();
runs and queues a render. The SC is free so the renderer services it's queue and renders the component.
If you're running on Blazor Server, you can offload your sync task to the threadpool. If not, then you're stuck with the SC.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论