【问题标题】:How to create a Task which always yields?如何创建一个总是产生的任务?
【发布时间】:2020-05-19 04:01:47
【问题描述】:

Task.Wait()Task.Result 相比,await 在 C# 5 中使用 Task 可防止执行等待的线程闲置。相反,使用await 关键字的方法需要是async,这样await 的调用只会使该方法返回一个代表async 方法执行的新任务。

但是当await'ed Taskasync 方法再次收到CPU 时间之前完成时,awaitTask 识别为已完成,因此async 方法将返回@987654335 @object 仅在以后使用。在某些情况下,这可能会晚于可接受的时间,因为开发人员假设await'ing 总是推迟其async 方法中的后续语句可能是一个常见错误。

错误的async 方法的结构可能如下所示:

async Task doSthAsync()
{
    var a = await getSthAsync();

    // perform a long operation
}

那么有时doSthAsync() 会在很长一段时间后才返回Task

我知道应该这样写:

async Task doSthAsync()
{
    var a = await getSthAsync();

    await Task.Run(() =>
    {
        // perform a long operation
    };
}

...或者那个:

async Task doSthAsync()
{
    var a = await getSthAsync();
    await Task.Yield();

    // perform a long operation
}

但我不觉得最后两个模式漂亮,想防止错误发生。我正在开发一个提供getSthAsync 的框架,第一个结构应该是通用的。所以 getSthAsync 应该返回一个 Awaitable ,它总是像 Task.Yield() 返回的 YieldAwaitable 那样产生。

不幸的是,任务并行库提供的大多数功能(如Task.WhenAll(IEnumerable<Task> tasks))仅在Tasks 上运行,因此getSthAsync 的结果应该是Task

那么是否有可能返回一个总是产生的Task

【问题讨论】:

    标签: c# .net multithreading asynchronous async-await


    【解决方案1】:

    首先,异步方法的使用者不应该假设它会“屈服”,因为这与异步无关。如果消费者需要确保有卸载到另一个线程,他们应该使用Task.Run 来强制执行。

    其次,我不明白使用Task.RunTask.Yield 有什么问题,因为它在返回Task 而不是YieldAwaitable 的异步方法中使用。

    如果您想创建一个行为类似于YieldAwaitableTask,您可以在异步方法中使用Task.Yield

    async Task Yield()
    {
        await Task.Yield();
    }
    

    编辑:

    正如 cmets 中提到的,这有一个竞争条件,它可能并不总是屈服。这种竞争条件是 TaskTaskAwaiter 的实现方式所固有的。为避免这种情况,您可以创建自己的 TaskTaskAwaiter

    public class YieldTask : Task
    {
        public YieldTask() : base(() => {})
        {
            Start(TaskScheduler.Default);
        }
    
        public new TaskAwaiterWrapper GetAwaiter() => new TaskAwaiterWrapper(base.GetAwaiter());
    }
    
    public struct TaskAwaiterWrapper : INotifyCompletion
    {
        private TaskAwaiter _taskAwaiter;
    
        public TaskAwaiterWrapper(TaskAwaiter taskAwaiter)
        {
            _taskAwaiter = taskAwaiter;
        }
    
        public bool IsCompleted => false;
        public void OnCompleted(Action continuation) => _taskAwaiter.OnCompleted(continuation);
        public void GetResult() => _taskAwaiter.GetResult();
    }
    

    这将创建一个 always 产生的任务,因为IsCompleted always 返回 false。可以这样使用:

    public static readonly YieldTask YieldTask = new YieldTask();
    
    private static async Task MainAsync()
    {
        await YieldTask;
        // something
    }
    

    注意:我强烈反对任何人实际做这种事情。

    【讨论】:

    • 使用没有问题;问题是,它需要完成(在我看来)。
    • 或者只是Task.Run(() => { })。这也应该有效。
    • @usr 为真,只有它有竞争条件,因为任务可能会在检查IsCompleted 之前完成。
    • 是的。但我认为,这对于您的 Yield 包装器也是如此。它现在让我的大脑超负荷。我认为 Yield() 返回的 TCS.Task 可以在 Yield() 返回之前标记为完成。
    • ...那你的答案就错了。任何人都可以确认这种竞争条件存在吗?
    【解决方案2】:

    这里是i3arnon'sYieldTask的完善版:

    public class YieldTask : Task
    {
        public YieldTask() : base(() => { },
            TaskCreationOptions.RunContinuationsAsynchronously)
            => RunSynchronously();
    
        public new YieldAwaitable.YieldAwaiter GetAwaiter()
            => default;
    
        public new YieldAwaitable ConfigureAwait(bool continueOnCapturedContext)
        {
            if (!continueOnCapturedContext) throw new NotSupportedException();
            return default;
        }
    }
    

    YieldTask 在创建后立即完成,但它的等待者另有说明。 GetAwaiter().IsCompleted 总是返回 false。这种恶作剧使await 运算符在每次等待此任务时触发所需的异步开关。实际上创建多个 YieldTask 实例是多余的。单例也可以。

    但这种方法存在问题。 Task 类的底层方法不是虚拟的,使用 new 修饰符隐藏它们意味着多态不起作用。如果您将YieldTask 实例存储到Task 变量中,您将获得默认的任务行为。这对我的用例来说是一个相当大的缺点,但我看不到任何解决方案。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-01-24
      • 1970-01-01
      • 1970-01-01
      • 2013-08-29
      • 1970-01-01
      • 2021-04-27
      相关资源
      最近更新 更多