缺乏非捕获 Task.Yield 迫使我使用 Task.Run,​​为什么要遵循呢? [英] The lack of non-capturing Task.Yield forces me to use Task.Run, why follow that?

查看:17
本文介绍了缺乏非捕获 Task.Yield 迫使我使用 Task.Run,​​为什么要遵循呢?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如果此问题是基于意见的,请提前道歉.缺乏 Task.Yield此处已经讨论了不会捕获执行上下文的版本.显然,此功能在早期版本的 Async CTP 中以某种形式存在,但由于很容易被误用而被删除.

Apologies in advance if this question is opinion-based. The lack of Task.Yield version which wouldn't capture the execution context was already discussed here. Apparently, this feature was present in some form in early versions of Async CTP but was removed because it could easily be misused.

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

IMO, such feature could be as easily misused as Task.Run itself. Here's what I mean. Imagine there's an awaitable SwitchContext.Yield API which schedules the continuation on ThreadPool, so the execution will always continues on a thread different from the calling thread. I could have used it in the following code, which starts some CPU-bound work from a UI thread. I would consider it a convenient way of continuing the CPU-bound work on a pool thread:

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 将如下所示.它以异步委托和任务嵌套的形式增加了一些额外的复杂性:

Now, without SwitchContext.Yield, DoWorkAsync would look like below. It adds some extra level of complexity in form of async delegate and task nesting:

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 实际上可能非常简单并且(我敢说)高效:

That said, implementing SwitchContext.Yield may actually be quite simple and (I dare to say) efficient:

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 被认为是不好的做法?

So, my question is, why should I prefer the second version of DoWorkAsync over the first one, and why would using SwitchContext.Yield be considered a bad practice?

推荐答案

您不必Task.Run 放入 DoWorkAsync>.考虑这个选项:

You don't have to put the Task.Run in DoWorkAsync. Consider this option:

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 将工作推送到后台线程上.

This results in code with much clearer intent. DoWorkAsync is a naturally synchronous method, so it has a synchronous signature. DoWorkAsync neither knows nor cares about the UI. The UIAction, which does care about the UI thread, pushes off the work onto a background thread using Task.Run.

作为一般规则,尽量将任何 Task.Run 调用从您的库方法中推送"出来.

As a general rule, try to "push" any Task.Run calls up out of your library methods as much as possible.

这篇关于缺乏非捕获 Task.Yield 迫使我使用 Task.Run,​​为什么要遵循呢?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆