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

How does Task.Yield work under the hood in Blazor WebAssembly?

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

澄清一下,我相信我已经很好地理解了 how Task.Yield works in .NET Framework and .NET Core. Mono implementation 看起来并没有太大的不同,简而言之,它归结为:

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

令人惊讶的是,这也适用于 Blazor WebAssembly (try it online):

<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's Asyncify, but eventually, does it use some sort of Web Platform API to schedule a continuation callback? And if so, which one exactly (like queueMicrotask, setTimout, Promise.resove().then 之类的东西)?


已更新,我刚刚发现 Thread.Sleep 也已实现,实际上 blocks the event loop thread

也想知道它在 WebAssembly 级别上是如何工作的。使用 JavaScript,我只能想到模拟 Thread.Sleep 的繁忙循环(因为 Atomics.wait 只能从网络工作线程获得)。

setTimeout。它与 QueueUserWorkItem 之间存在相当大的间接关系,但这是它触底的地方。

大多数特定于 WebAssembly 的机制都可以在 PR 38029. The WebAssembly implementation of RequestWorkerThread calls a private method named QueueCallback, which is implemented in C code as mono_wasm_queue_tp_cb. This in invokes mono_threads_schedule_background_job, which in turn calls 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特有的,可以通过阅读ThreadPoolWorkQueueclass的源代码进行研究。简而言之,ThreadPool.QueueUserWorkItem 将回调放入 ThreadPoolQueue 队列中。排队调用 EnsureThreadRequested,委托给 RequestWorkerThread,实现如上。 ThreadPoolWorkQueue.Dispatch 导致一些异步任务出列并执行;其中,传递给QueueUserWorkItem的回调应该最终出现。