Task.Yield如何在Blazor WebAssembly中工作?



Task.Yield如何在 Mono/WASM 运行时(由 Blazor WebAssembly 使用)中工作?

澄清一下,我相信我对Task.Yield如何在.NET Framework和.NET Core中工作有很好的理解。Mono 实现看起来没有太大不同,简而言之,它归结为:

static Task Yield() 
{
var tcs = new TaskCompletionSource<bool>();
System.Threading.ThreadPool.QueueUserWorkItem(_ => tcs.TrySetResult(true));
return tcs.Task;
}

令人惊讶的是,这也适用于Blazor WebAssembly(在线尝试):

<label>Tick Count: @tickCount</label><br>
@code 
{
int tickCount = System.Environment.TickCount;
protected override void OnAfterRender(bool firstRender)
{
if (firstRender) CountAsync();
}
static Task Yield() 
{
var tcs = new TaskCompletionSource<bool>();
System.Threading.ThreadPool.QueueUserWorkItem(_ => tcs.TrySetResult(true));
return tcs.Task;
}
async void CountAsync() 
{
for (var i = 0; i < 10000; i++) 
{
await Yield();
tickCount = System.Environment.TickCount;
StateHasChanged();
}
}
}

当然,这一切都发生在浏览器中的同一个事件循环线程上,所以我想知道它在较低级别是如何工作的。

我怀疑,它可能正在使用类似Emscripten的Asyncify的东西,但最终,它是否使用某种Web平台API来安排继续回调?如果是这样,究竟是哪一个(如queueMicrotasksetTimoutPromise.resove().then等)?


更新后,我刚刚发现Thread.Sleep也实现了,它实际上阻止了事件循环线程

对它在WebAssembly级别的工作方式也很好奇。使用 JavaScript,我只能想到一个繁忙的循环来模拟Thread.Sleep(因为Atomics.wait只能从 Web worker 线程中获得)。

这是setTimeout. 这和QueueUserWorkItem之间有相当大的间接性,但这是它触底的地方。

大多数特定于 WebAssembly 的机制都可以在 PR 38029 中看到。RequestWorkerThread的 WebAssembly 实现调用一个名为QueueCallback的私有方法,该方法在 C 代码中实现为mono_wasm_queue_tp_cb。 这调用了mono_threads_schedule_background_job,而又调用了schedule_background_exec,在TypeScript中实现为:

export function schedule_background_exec(): void {
++pump_count;
if (typeof globalThis.setTimeout === "function") {
globalThis.setTimeout(pump_message, 0);
}
}

setTimeout回调最终到达ThreadPool.Callback,调用ThreadPoolWorkQueue.Dispatch

其余部分根本不特定于 Blazor,可以通过阅读ThreadPoolWorkQueue类的源代码来研究。 简而言之,ThreadPool.QueueUserWorkItemThreadPoolQueue中对回调进行排队。 排队调用EnsureThreadRequested,委托给RequestWorkerThread,实现如上。ThreadPoolWorkQueue.Dispatch会导致一些异步任务取消排队并执行;其中,传递给QueueUserWorkItem的回调最终应该会出现。

最新更新