缺乏非捕获的Task.Yield迫使我使用Task.Run,为什么要遵循它

本文关键字:Task 为什么 Run Yield | 更新日期: 2023-09-27 18:31:06

如果这个问题是基于意见的,请提前道歉。这里已经讨论了缺少无法捕获执行上下文的 Task.Yield 版本。显然,此功能在异步 CTP 的早期版本中以某种形式存在,但由于它很容易被滥用而被删除

IMO,这样的功能可能像Task.Run本身一样容易被滥用。这就是我的意思。假设有一个可等待的 SwitchContext.Yield API,它在 ThreadPool 上调度继续,因此执行将始终在与调用线程不同的线程上继续。我本可以在以下代码中使用它,该代码从 UI 线程启动一些 CPU 密集型工作。我认为这是在池线程上继续 CPU 密集型工作的便捷方法:

class Worker
{
    static void Log(string format, params object[] args)
    {
        Debug.WriteLine("{0}: {1}", Thread.CurrentThread.ManagedThreadId, String.Format(format, args));
    }
    public async Task UIAction()
    {
        // UI Thread
        Log("UIAction");
        // start the CPU-bound work
        var cts = new CancellationTokenSource(5000);
        var workTask = DoWorkAsync(cts.Token); 
        // possibly await for some IO-bound work 
        await Task.Delay(1000);
        Log("after Task.Delay");
        // finally, get the result of the CPU-bound work
        int c = await workTask;
        Log("Result: {0}", c);
    }
    async Task<int> DoWorkAsync(CancellationToken ct)
    {
        // start on the UI thread
        Log("DoWorkAsync");
        // switch to a pool thread and yield back to the UI thread
        await SwitchContext.Yield();
        Log("after SwitchContext.Yield");
        // continue on a pool thread
        int c = 0;
        while (!ct.IsCancellationRequested)
        {
            // do some CPU-bound work on a pool thread: counting cycles :)
            c++;
            // and use async/await too
            await Task.Delay(50);
        }
        return c;
    }
}

现在,如果没有SwitchContext.YieldDoWorkAsync如下所示。它以异步委托和任务嵌套的形式增加了一些额外的复杂性:

async Task<int> DoWorkAsync(CancellationToken ct)
{
    // start on the UI thread
    Log("DoWorkAsync");
    // Have to use async delegate
    // Task.Run uwraps the inner Task<int> task
    return await Task.Run(async () =>
    {
        // continue on a pool thread
        Log("after Task.Yield");
        int c = 0;
        while (!ct.IsCancellationRequested)
        {
            // do some CPU-bound work on a pool thread: counting cycles :)
            c++;
            // and use async/await too
            await Task.Delay(50);
        }
        return c;
    });
}

也就是说,实现SwitchContext.Yield实际上可能非常简单且(我敢说)高效:

public static class SwitchContext
{
    public static Awaiter Yield() { return new Awaiter(); }
    public struct Awaiter : System.Runtime.CompilerServices.INotifyCompletion
    {
        public Awaiter GetAwaiter() { return this; }
        public bool IsCompleted { get { return false; } }
        public void OnCompleted(Action continuation)
        {
            ThreadPool.QueueUserWorkItem((state) => ((Action)state)(), continuation);
        }
        public void GetResult() { }
    }
}

所以,我的问题是,为什么我更喜欢DoWorkAsync的第二个版本而不是第一个版本,为什么使用SwitchContext.Yield被认为是一种不好的做法?

缺乏非捕获的Task.Yield迫使我使用Task.Run,为什么要遵循它

不必Task.Run放在DoWorkAsync中。请考虑此选项:

public async Task UIAction()
{
    // UI Thread
    Log("UIAction");
    // start the CPU-bound work
    var cts = new CancellationTokenSource(5000);
    var workTask = Task.Run(() => DoWorkAsync(cts.Token)); 
    // possibly await for some IO-bound work 
    await Task.Delay(1000);
    Log("after Task.Delay");
    // finally, get the result of the CPU-bound work
    int c = await workTask;
    Log("Result: {0}", c);
}

这会导致代码具有更清晰的意图。 DoWorkAsync 是一种自然同步方法,因此它具有同步签名。 DoWorkAsync既不知道也不关心 UI。UIAction确实关心 UI 线程,使用 Task.Run 将工作推送到后台线程上。

作为一般规则,尝试尽可能将任何Task.Run调用"推送"出库方法。