繁体   English   中英

“await Task.Yield()”及其替代方案

[英]"await Task.Yield()" and its alternatives

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

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

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

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

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

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

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

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

这是一个示例,其中只有对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);
        }
    }
}

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

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

现有同步方法:

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;

与如果您使用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();

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

Task.Yield()真正有用的一种情况是当您await递归调用同步完成的Task时。 因为 csharp 的async / await通过尽可能同步运行延续来“释放 Zalgo” ,所以完全同步递归场景中的堆栈可以变得足够大,以至于您的进程死亡。 我认为这也部分是由于Task间接导致无法支持尾调用。 await Task.Yield()安排由调度程序而不是内联运行延续,从而避免堆栈增长并解决此问题。

此外, Task.Yield()可用于缩短方法的同步部分。 如果调用者需要在您的方法执行某些操作之前接收您的方法的Task ,您可以使用Task.Yield()强制提前返回Task ,否则会自然发生。 例如,在以下本地方法场景中, async方法能够安全地获取对其自身Task的引用(假设您在单并发SynchronizationContext上运行它,例如在 winforms 中或通过nito 的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方法的同步部分是做某事的最佳方式。 知道你可以像我刚刚展示的那样做一个技巧有时会很有用,但它也往往更危险。 通常,您可以以更好、更易读和更线程安全的方式传递数据。 例如,您可以使用TaskCompletionSource向本地方法传递对其自身Task的引用:

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

Task.Yield不是Task.Factory.StartNewTask.Run的替代品。 他们完全不同。 当您await Task.Yield时,您允许当前线程上的其他代码在不阻塞线程的情况下执行。 把它想象成等待Task.Delay ,除了Task.Yield等到任务完成,而不是等待一个特定的时间。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM