"等待Task.Yield()"及其替代品

时间:2013-12-02 02:08:34

标签: c# .net task-parallel-library async-await

如果我需要推迟代码执行,直到UI线程消息循环的未来迭代之后,我可以这样做:

await Task.Factory.StartNew(
    () => {
        MessageBox.Show("Hello!");
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    TaskScheduler.FromCurrentSynchronizationContext());

这与await Task.Yield(); MessageBox.Show("Hello!");类似,此外如果我愿意,我还可以选择取消任务。

对于默认同步上下文,我可以类似地使用await Task.Run继续池线程。

事实上,我更喜欢Task.Factory.StartNewTask.Run而不是Task.Yield,因为它们都明确定义了延续代码的范围。

那么,在await Task.Yield()实际有用的情况下呢?

4 个答案:

答案 0 :(得分:6)

考虑您希望异步任务返回值的情况。

现有的同步方法:

public int DoSomething()
{
    return SomeMethodThatReturnsAnInt();
}

要进行异步,请添加async关键字并更改返回类型:

public async Task<int> DoSomething()

要使用Task.Factory.StartNew(),请将方法的单行主体更改为:

// start new task
var task = Task<int>.Factory.StartNew(
    () => {
        return SomeMethodThatReturnsAnInt();
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    TaskScheduler.FromCurrentSynchronizationContext() );

// await task, return control to calling method
await task;

// return task result
return task.Result;

VS。如果您使用await Task.Yield()

,请添加一行
// this returns control to the calling method
await Task.Yield();

// otherwise synchronous method scheduled for async execution by the 
// TaskScheduler of the calling thread
return SomeMethodThatReturnsAnInt();

后者更简洁,更易读,并且实际上并没有太多改变现有方法。

答案 1 :(得分:4)

Task.Yield()非常适合在async方法的其他同步部分中“打洞”。

我个人认为,在我有一个自我取消async方法(一个管理自己对应的CancellationTokenSource并在每次后续调用时取消之前创建的实例)的情况下,它会很有用在极短的时间内被多次调用(即通过相互依赖的UI元素的事件处理程序)。在这种情况下,一旦Task.Yield()被换出,使用IsCancellationRequested后跟CancellationTokenSource检查可以防止执行可能会导致结果最终会被丢弃的昂贵工作。

这是一个示例,其中只有对SelfCancellingAsync的最后一次排队调用才能执行昂贵的工作并运行完成。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TaskYieldExample
{
    class Program
    {
        private static CancellationTokenSource CancellationTokenSource;

        static void Main(string[] args)
        {
            SelfCancellingAsync();
            SelfCancellingAsync();
            SelfCancellingAsync();

            Console.ReadLine();
        }

        private static async void SelfCancellingAsync()
        {
            Console.WriteLine("SelfCancellingAsync starting.");

            var cts = new CancellationTokenSource();
            var oldCts = Interlocked.Exchange(ref CancellationTokenSource, cts);

            if (oldCts != null)
            {
                oldCts.Cancel();
            }

            // Allow quick cancellation.
            await Task.Yield();

            if (cts.IsCancellationRequested)
            {
                return;
            }

            // Do the "meaty" work.
            Console.WriteLine("Performing intensive work.");

            var answer = await Task
                .Delay(TimeSpan.FromSeconds(1))
                .ContinueWith(_ => 42, TaskContinuationOptions.ExecuteSynchronously);

            if (cts.IsCancellationRequested)
            {
                return;
            }

            // Do something with the result.
            Console.WriteLine("SelfCancellingAsync completed. Answer: {0}.", answer);
        }
    }
}

这里的目标是允许在非等待的异步方法调用之后立即在同一SynchronizationContext上同步执行的代码(当它命中第一个await时)更改状态这会影响异步方法的执行。这与Task.Delay(我在这里谈论非零延迟期)所取得的节流非常相似,但没有实际,可能会出现明显的延迟,这在一些情况下可能不受欢迎的情况。

答案 2 :(得分:1)

Task.Yield()实际上有用的一种情况是await recursively-called synchronously-completed Tasks。因为csharp的async / await “releases Zalgo”通过在可能的情况下同步运行continuation,所以完全同步递归场景中的堆栈可以变得足够大以至于您的进程死亡。我认为这也部分是由于Task间接导致无法支持尾调用。 await Task.Yield()计划由调度程序而不是内联运行继续,允许避免堆栈中的增长,并解决此问题。

此外,Task.Yield()可用于缩短方法的同步部分。如果调用者需要在方法执行某些操作之前收到方法的Task,则可以使用Task.Yield()强制返回Task,而不是自然发生。例如,在以下本地方法方案中,async方法能够安全地获取对其自己的Task的引用(假设您在单并发SynchronizationContext上运行此方法,例如在winforms中或通过nito’s AsyncContext.Run()):

using Nito.AsyncEx;
using System;
using System.Threading.Tasks;

class Program
{
    // Use a single-threaded SynchronizationContext similar to winforms/WPF
    static void Main(string[] args) => AsyncContext.Run(() => RunAsync());

    static async Task RunAsync()
    {
        Task<Task> task = null;
        task = getOwnTaskAsync();
        var foundTask = await task;
        Console.WriteLine($"{task?.Id} == {foundTask?.Id}: {task == foundTask}");

        async Task<Task> getOwnTaskAsync()
        {
            // Cause this method to return and let the 「task」 local be assigned.
            await Task.Yield();
            return task;
        }
    }
}

输出:

3 == 3: True

我很抱歉,我无法想到任何能够强行缩短async方法的同步部分的现实生活场景是做某事的最佳方式。知道你可以像我刚刚展示的那样做一些技巧有时会很有用,但它也往往更危险。通常,您可以以更好,更易读,更线程安全的方式传递数据。例如,您可以使用Task将本地方法的引用传递给自己的TaskCompletionSource

using System;
using System.Threading.Tasks;

class Program
{
    // Fully free-threaded! Works in more environments!
    static void Main(string[] args) => RunAsync().Wait();

    static async Task RunAsync()
    {
        var ownTaskSource = new TaskCompletionSource<Task>();
        var task = getOwnTaskAsync(ownTaskSource.Task);
        ownTaskSource.SetResult(task);
        var foundTask = await task;
        Console.WriteLine($"{task?.Id} == {foundTask?.Id}: {task == foundTask}");

        async Task<Task> getOwnTaskAsync(
            Task<Task> ownTaskTask)
        {
            // This might be clearer.
            return await ownTaskTask;
        }
    }
}

输出:

2 == 2: True

答案 3 :(得分:-1)

Task.Yield不能替代Task.Factory.StartNewTask.Run。他们完全不同。当您await Task.Yield时,您允许当前线程上的其他代码执行而不阻塞该线程。可以把它想象为等待Task.Delay,除了Task.Yield等待任务完成,而不是等待特定的时间。

注意:不要在UI线程上使用Task.Yield并假设UI始终保持响应。情况并非总是如此。