如何创建一个总是产生的任务?

How to create a Task which always yields?

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

但是当 awaitTaskasync 方法再次收到 CPU 时间之前完成时,await 识别 Task 完成,因此 async 方法将 return Task 对象仅在稍后的时间。在某些情况下,这可能会晚于可接受的时间,因为开发人员假设 await 总是推迟他的 async 方法中的后续语句可能是一个常见的错误。

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

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

    // perform a long operation
}

然后有时 doSthAsync() 会 return 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 应该 return 一个 Awaitable,它总是像 Task.Yield() 编辑的 YieldAwaitable return 那样产生。

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

那么有没有可能 return 一个 Task 总是 yield?

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

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

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

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

编辑:

正如评论中提到的,这有一个竞争条件,它可能并不总是屈服。这种竞争条件是 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 returns false。可以这样使用:

public static readonly YieldTask YieldTask = new YieldTask();

private static async Task MainAsync()
{
    await YieldTask;
    // something
}

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

这是 YieldTask 的完善版本:

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总是returnsfalse。这个恶作剧使得 await 运算符每次等待此任务时都会触发所需的异步切换。实际上创建多个 YieldTask 实例是多余的。单例也可以。

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