【问题标题】:"await Task.Yield()" and its alternatives“await Task.Yield()”及其替代方案
【发布时间】:2013-12-17 15:47:10
【问题描述】:

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

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

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

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

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

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

【问题讨论】:

  • 我知道你的问题没有问这个,但它有点相关。如果在您的窗口没有焦点时执行该代码,则调用 MessageBox.Show() 而不传递 IWin32Window owner argument 可能会导致消息框“弹出”您的窗口。如果在 GUI 线程上完成,这尤其令人困惑。此外,如果您确实将IWin32Window 传递给MessageBox.Show(),则需要在UI 线程上这样做。因此,在这种情况下,您不得使用Task.Run(),并且必须TaskScheduler传递给StartNew()
  • 此外,没有必要将MessageBox.Show() 放在单独的线程上,因为MessageBox.Show() 泵送消息队列并运行计划到GUI 的SynchronizationContext 的延续。即,即使在显示 MessageBox.Show() 时,您也可以像往常一样继续更新表单并显示 async 方法。

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


【解决方案1】:

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

现有同步方式:

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();

后者更加简洁、易读,并且对现有的方法并没有太大的改变。

【讨论】:

  • 公平地说,它仍然是一行:return await Task.Factory.StartNew(() =&gt; SomeMethodThatReturnsAnInt(), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());。不过,我明白了这一点,+1。问题在于控制流的一些不直观的改变,讨论了here
  • 好吧,你还需要await task;这一行
  • 重温一下,如果我需要将SomeMethodThatReturnsAnInt 变成async,我可以简单地这样做:public Task&lt;int&gt; DoSomething() { return Task.FromResult(SomeMethodThatReturnsAnInt()); }。或者,对于async semantic of exception propagationpublic async Task&lt;int&gt; DoSomething() { return await Task.FromResult(SomeMethodThatReturnsAnInt()); }。显然,await Task.Yield() 在这里是多余且不受欢迎的。很抱歉撤消了我的投票。
  • simply suppress the CS1998 warning 比仅仅为了间接抑制编译器警告而强制上下文切换要好得多。
  • @noseratio Task.FromResult(SomeMethodThatReturnsAnInt()); 仍然同步执行。 FromResult 和对应方返回已完成的任务,因此不会发生上下文暂停。另一方面,Yield 暂停当前方法,直到事件循环空闲并从那里恢复。有时需要后一种行为。
【解决方案2】:

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

就我个人而言,我发现它在我有一个可以被多次调用的自我取消 async 方法(管理自己对应的 CancellationTokenSource 并在每次后续调用时取消先前创建的实例的方法)的情况下很有用在极短的时间内(即通过相互依赖的 UI 元素的事件处理程序)。在这种情况下,使用Task.Yield(),然后在CancellationTokenSource 被换出后立即检查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 时)更改影响执行的状态的异步方法。这与Task.Delay 实现的限制非常相似(我在这里谈论的是非零延迟期),但没有实际,可能会出现明显的延迟,这在某些情况下可能不受欢迎。

【讨论】:

  • 感谢您的想法,尽管我在没有Task.Yield 或助手Task.Delayhere's my version 的取消和重新启动模式的情况下也这样做。
  • 顺便说一句,由于SelfCancellingAsyncasync void 签名,您可能在处理“大量”工作中的异常时遇到问题。
  • 为了简洁起见,我特别处理任何异常 - 这是选择使用该模式的人的工作。该示例必须是void,因为将其设置为Task 表明应该等待它,但这里不是这种情况。这也是为什么我在检测到取消而不是抛出异常时从该方法返回的原因(这通常是向调用者传达取消的首选方式)。
  • 没错,这是两种异步方法之间的关键区别。每当您有async Task 时,抛出异常并让调用者处理它们是处理事情的首选方式。然而,在async void 的情况下,异常处理通常需要连接到异步方法本身的主体中。
  • 我想如果您的任务完全隔离并且您在SelfCancellingAsync 中处理所有 异常可能没问题。但是,最终您会从外部请求取消(例如,当您的 Main 方法退出并且应用程序终止时)。此时,您可能希望等待由SelfCancellingAsync 启动的待处理任务,让它优雅地完成。我看不出你的方法是如何做到的。
【解决方案3】:

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

另外,Task.Yield() 可用于缩短方法的同步部分。如果调用者需要在您的方法执行某些操作之前接收您的方法的Task,您可以使用Task.Yield() 强制返回Task,否则会自然发生。例如,在以下本地方法场景中,async 方法能够安全地获取对自己的 Task 的引用(假设您在单并发 SynchronizationContext 上运行它,例如在 winforms 中或通过 @987654323 @):

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

【讨论】:

  • 处理递归确实是一个很好的用途,尽管它的成本很大程度上取决于特定的同步上下文。
  • @noseratio 是的。它有开销的事实可以通过传递一个计数器并且每 30 次左右递归才使用它来稍微减轻一点。不过,我不太确定SynchronizationContext 与它有什么关系。如果您正在做这样的事情,希望它不在图形应用程序中的 GUI 线程上。但是,如果您经常使用任务 API,您可能不确定您正在等待的任务尚未完成,因此可能有必要这样做。您始终可以将 Task 设为 Task.IsCompleted 和一个计数器作为条件。
【解决方案4】:

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

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

【讨论】:

  • 我相信我对Task.Yield 和其他自定义等待者的工作方式有很好的理解。鉴于此,除了你的最后一句话,我几乎不同意其他所有内容。
  • “除了Task.Yield 等到任务完成,而不是等待特定时间。”——它不等待其他任务完成。它只是安排一个“准备运行”的延续并切换到下一个现有的“准备运行”的延续,它正在排队等待(可能是它自己)。也就是说,这只会迫使您的任务在那时停止同步。
猜你喜欢
  • 2021-09-07
  • 2017-05-05
  • 2014-07-08
  • 2011-07-24
  • 2011-10-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多