Task.Yield - 实际用途?

Posted

技术标签:

【中文标题】Task.Yield - 实际用途?【英文标题】:Task.Yield - real usages? 【发布时间】:2014-06-19 08:12:22 【问题描述】:

我一直在阅读有关 Task.Yield 的信息,作为一名 javascript 开发人员,我可以说它的工作是 完全setTimeout(function ()...,0); 在让主单线程处理其他东西,也就是:

“不要把所有的力量,从时间中释放出来——这样别人就会 也有一些……”

在 js 中,它特别适用于长循环。 (不要让浏览器冻结...

但我看到了这个例子here:

public static async Task < int > FindSeriesSum(int i1)

    int sum = 0;
    for (int i = 0; i < i1; i++)
    
        sum += i;
        if (i % 1000 == 0) ( after a bulk , release power to main thread)
            await Task.Yield();
    

    return sum;

作为一名 JS 程序员,我可以理解他们在这里做了什么。

但作为 C# 程序员,我问自己:为什么不为它打开一个任务?

 public static async Task < int > FindSeriesSum(int i1)
    
         //do something....
         return await MyLongCalculationTask();
         //do something
    

问题

使用 Js 我无法打开任务(是的,我知道我实际上可以使用网络工作者)。但是使用 c#我可以

如果是这样——既然我可以发布,为什么还要不时发布呢?

编辑

添加参考:

来自here:

来自here(另一本电子书):

【问题讨论】:

这是一个糟糕的例子,因为Task.Yield 不会保持 UI 响应。此外,asyncawait 不会释放 UI 线程;如果计算时间较长,则需要使用Task.Run @StephenCleary 考虑一个多线程应用程序(ASP.NET 或类似的),其中一个任务已经在线程池上执行并且线程池是一种宝贵的资源:如果长时间运行的 CPU 密集型任务会每秒调用await Task.Yield(),然后其他短时间运行的任务应该有机会在队列中等待很长时间。这不是公平的行为吗? @springy76:每个现代操作系统中内置的抢占式线程切换与 .NET 中的自调整线程池相结合,将比手动 await Task.Yield() 工作得更好。 【参考方案1】:

您假设长时间运行的函数可以在后台线程上运行。如果不是,例如因为它具有 UI 交互,则无法防止在 UI 运行时阻塞 UI,因此它的运行时间应保持足够短,以免给用户造成问题。

另一种可能性是,您拥有的长时间运行的函数比后台线程的数量多。在这种情况下,防止其中一些函数占用所有线程可能会更好(或者可能无关紧要,这取决于)。

【讨论】:

您的第一个案例可能有效,但第二个案例确实不是。如果你有更多的工作,甚至是工人,而不是你的线程,那么他们只需要分享那个时间。将其中一些工作交给 UI 线程并没有帮助。如果你不这样做,那只是意味着 UI 线程将处于空闲状态,从而将时间留给工作线程。 @hvd 通过说 UI 交互 - 您正在谈论可以出现(之前 || 之后)await 的代码。对吗? @hvd 你需要有比线程多很多个操作才能实现。这称为线程饥饿。除了最极端的情况外,线程的调度程序能够通过更改每个线程上的执行来解决问题,允许每个短片运行,而不是运行每个操作完成。除了之外,可以通过使用线程池更有效地解决问题,线程池可以拥有与可选的一样多的工作人员,并将实际工作排队等待在这样的情况下完成一种优化吞吐量的方法。 @hvd 不,这不会导致问题,因为线程的调度程序并没有那么差,而且 UI 线程需要被饿死以供人类使用的时间量感觉滞后是相当高的。其他线程没有选择永远占用 CPU 时间;事实上,他们对此事几乎没有任何控制权。它不是一个协作系统,与 从 UI 线程内管理 UI 线程不同,在这种系统中,有人可以有意地占用 UI 线程,只要他们愿意,让其他人挨饿。 @hvd 从技术上讲,这只是实现我提到的第二个选项的一种方式,它只是一种设计相当糟糕的实现方式。如果一项任务非常适合分解,那么就分解它。如果不是,请在上面贴上LongRunning,而不是到处贴yield【参考方案2】:

不,这与使用 setTimeout 将控制权返回给 UI 完全不同。在始终让 UI 更新的 JavaScript 中,setTimeout 始终具有几毫秒的最小暂停,并且待处理的 UI 工作优先于计时器,但 await Task.Yield(); 不会这样做。

不能保证 yield 会让任何工作在主线程中完成,相反,调用 yield 的代码通常会优先于 UI 工作。

"在大多数 UI 中,UI 线程上存在的同步上下文 环境通常会优先考虑发布到更高上下文的工作 而不是输入和渲染工作。因此,不要依赖 await Task.Yield();保持 UI 响应。”

参考:MSDN: Task.Yield Method

【讨论】:

如果不依赖它 - 那么它的存在是什么?你能提供真实的例子吗? @RoyiNamir:它用于处理多线程,但在保持 UI 响应方面并不可靠。它使方法异步,但不会阻止它与主线程竞争 CPU。 如果它可以赢得主线程优先级,那我为什么要使用它?你的意思是代码可能运行同步而不是异步 @RoyiNamir:代码异步运行,但由于工作的优先顺序,您可能会觉得它同步运行。异步任务更适合等待某事发生的工作,对于 CPU 密集型工作,您宁愿研究 AsParallel 方法。【参考方案3】:

首先让我澄清一下:YieldsetTimeout(function ()...,0); 并不完全相同。 JS 在单线程环境中执行,所以这是让其他活动发生的唯一方法。有点像cooperative multitasking。 .net 在具有显式多线程的抢占式多任务环境中执行。

现在回到Thread.Yield。正如我所说,.net 生活在抢占式世界中,但它比这更复杂一些。 C#await/async 创造了由状态机统治的多任务模式的有趣混合。因此,如果您在代码中省略 Yield,它只会阻塞线程,仅此而已。如果您将其设为常规任务并仅调用 start (或线程),那么它将仅并行执行它的工作,然后在调用 task.Result 时阻止调用线程。当你做await Task.Yield(); 时会发生什么更复杂。从逻辑上讲,它会解锁调用代码(类似于 JS)并继续执行。它实际上做了什么 - 它选择另一个线程并在具有调用线程的抢占环境中继续执行。所以它一直在调用线程,直到第一个 Task.Yield 然后它是独立的。随后对Task.Yield 的调用显然没有任何作用。

简单演示:

class MainClass

    //Just to reduce amont of log itmes
    static HashSet<Tuple<string, int>> cache = new HashSet<Tuple<string, int>>();
    public static void LogThread(string msg, bool clear=false) 
        if (clear)
            cache.Clear ();
        var val = Tuple.Create(msg, Thread.CurrentThread.ManagedThreadId);
        if (cache.Add (val))
            Console.WriteLine ("0\t:1", val.Item1, val.Item2);
    

    public static async Task<int> FindSeriesSum(int i1)
    
        LogThread ("Task enter");
        int sum = 0;
        for (int i = 0; i < i1; i++)
        
            sum += i;
            if (i % 1000 == 0) 
                LogThread ("Before yield");
                await Task.Yield ();
                LogThread ("After yield");
            
        
        LogThread ("Task done");
        return sum;
    

    public static void Main (string[] args)
    
        LogThread ("Before task");
        var task = FindSeriesSum(1000000);
        LogThread ("While task", true);
        Console.WriteLine ("Sum = 0", task.Result);
        LogThread ("After task");
    

结果如下:

Before task     :1
Task enter      :1
Before yield    :1
After yield     :5
Before yield    :5
While task      :1
Before yield    :5
After yield     :5
Task done       :5
Sum = 1783293664
After task      :1
在 Mac OS X 上的单声道 4.5 上产生的输出,结果可能因其他设置而异

如果您将Task.Yield 移动到方法的顶部,它将从一开始就异步并且不会阻塞调用线程。

结论:Task.Yield 可以混合同步和异步代码。一些或多或少的现实场景:你有一些繁重的计算操作和本地缓存和任务CalcThing。在此方法中,您检查项目是否在缓存中,如果是 - 返回项目,如果它不存在 Yield 并继续计算它。实际上,您书中的样本毫无意义,因为那里没有取得任何有用的成果。他们关于 GUI 交互性的评论很糟糕而且不正确(UI 线程将被锁定,直到第一次调用 Yield,你永远不应该这样做,MSDN 很清楚(并且正确)这一点:“不要依赖等待任务。 Yield(); 保持 UI 响应”。

【讨论】:

与 JS 的类比是在 c# 中的 GUI 应用程序有一个运行 UI 的单个主线程的上下文中——与 JS 单循环相同。 (在 JS 中 - 解决方案是 setTimeout(...,0 ) 喜欢这个演示 :)【参考方案4】:

我只发现Task.Yield 在两种情况下有用:

    单元测试,以确保被测代码在异步情况下正常工作。 To work around an obscure ASP.NET issue where identity code cannot complete synchronously。

【讨论】:

【参考方案5】:

当你看到:

await Task.Yield();

你可以这样想:

await Task.Factory.StartNew( 
    () => , 
    CancellationToken.None, 
    TaskCreationOptions.None, 
    SynchronizationContext.Current != null?
        TaskScheduler.FromCurrentSynchronizationContext(): 
        TaskScheduler.Current);

所有这一切都是为了确保继续在未来异步发生。 异步我的意思是执行控制将返回给async方法的调用者,而继续回调将不会发生在同一个堆栈帧上。

具体何时发生以及发生在哪个线程上完全取决于调用者线程的同步上下文。

对于 UI 线程,继续将在消息循环的某些未来迭代中发生,由 Application.Run (WinForms) 或 Dispatcher.Run (WPF) 运行。在内部,它归结为 Win32 PostMessage API,它将自定义消息发布到 UI 线程的消息队列。 await 继续回调将在此消息被抽取和处理时调用。你完全无法控制这到底什么时候会发生。

此外,Windows 有其自己的消息发送优先级:INFO: Window Message Priorities。最相关的部分:

在这个方案下,优先级可以被认为是三级的。全部 发布消息的优先级高于用户输入消息,因为 他们驻留在不同的队列中。并且所有用户输入消息都是 优先级高于 WM_PAINT 和 WM_TIMER 消息。

因此,如果您使用await Task.Yield() 屈服于消息循环以试图保持 UI 响应,那么您实际上有阻碍 UI 线程的消息循环的风险。一些待处理的用户输入消息以及WM_PAINTWM_TIMER 的优先级低于已发布的继续消息。因此,如果您在紧密循环中执行 await Task.Yield(),您仍然可能会阻塞 UI。

这就是它与您在问题中提到的 JavaScript 的 setTimer 类比的不同之处。在浏览器的消息泵处理完所有用户输入消息后,将调用setTimer 回调

所以,await Task.Yield() 不适合在 UI 线程上进行后台工作。事实上,你很少需要在 UI 线程上运行后台进程,但有时你会这样做,例如编辑器语法高亮、拼写检查等。在这种情况下,请使用框架的空闲基础设施。

例如,使用 WPF 你可以做到await Dispatcher.Yield(DispatcherPriority.ApplicationIdle):

async Task DoUIThreadWorkAsync(CancellationToken token)

    var i = 0;

    while (true)
    
        token.ThrowIfCancellationRequested();

        await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);

        // do the UI-related work item
        this.TextBlock.Text = "iteration " + i++;
    

对于 WinForms,您可以使用 Application.Idle 事件:

// await IdleYield();

public static Task IdleYield()

    var idleTcs = new TaskCompletionSource<bool>();
    // subscribe to Application.Idle
    EventHandler handler = null;
    handler = (s, e) =>
    
        Application.Idle -= handler;
        idleTcs.SetResult(true);
    ;
    Application.Idle += handler;
    return idleTcs.Task;

It is recommended 在 UI 线程上运行的此类后台操作的每次迭代不超过 50 毫秒。

对于没有同步上下文的非 UI 线程await Task.Yield() 只是将延续切换到随机池线程。不能保证它会是一个与当前线程不同的 线程,它只能保证是一个异步 延续。如果ThreadPool 处于饥饿状态,它可能会将继续调度到同一个线程上。

在 ASP.NET 中,除了 @StephenCleary's answer 中提到的解决方法之外,执行 await Task.Yield() 完全没有意义。否则,它只会通过冗余线程切换而损害 Web 应用程序的性能。

那么,await Task.Yield() 有用吗? 国际海事组织,不多。如果您确实需要在方法的一部分上施加异步,它可以用作通过SynchronizationContext.PostThreadPool.QueueUserWorkItem 运行延续的快捷方式。

关于您引用的书籍,我认为那些使用Task.Yield 的方法是错误的。我在上面解释了为什么它们对于 UI 线程是错误的。对于非 UI 池线程,根本没有“线程中要执行的其他任务”,除非您运行像 Stephen Toub's AsyncPump 这样的自定义任务泵。

更新回答评论:

... 怎么可能是异步操作并留在同一个线程中 ?..

举个简单的例子:WinForms 应用:

async void Form_Load(object s, object e) 
 
    await Task.Yield(); 
    MessageBox.Show("Async message!");

Form_Load 将返回给调用者(已触发 Load 事件的 WinFroms 框架代码),然后在 Application.Run() 运行的消息循环的某些未来迭代中,消息框将异步显示。延续回调与WinFormsSynchronizationContext.Post 一起排队,它在内部将私有 Windows 消息发布到 UI 线程的消息循环。当此消息被抽出时,回调将被执行,仍然在同一个线程上。

在控制台应用程序中,您可以使用上面提到的AsyncPump 运行类似的序列化循环。

【讨论】:

鼻塞,嗨 :-)。你说:“不能保证它会是与当前线程不同的线程,它只能保证是异步延续”。 ——但如果它留在同一个线程上——操作就变成了同步操作。不 ?换句话说,它怎么可能是异步操作并留在同一个线程中? asynchroonsey 是关于延续 - 意思是“我会回来并继续它停止的地方”。但如果它在同一个线程中,它更有可能从未离开过选项.....不? 嘿@RoyiNamir,很有可能在同一个线程上异步继续,检查我的更新。 Yes :) ,但是你在non-UI thread部分写了评论,这让我很困惑..... @RoyiNamir,您也可以在非 UI 线程上执行此操作,具有 SynchronizationContext 的魔力。你看过AsyncPump吗?毕竟,异步不假设多线程。这只是将来会产生结果的东西。请注意,即使您在没有任何 s.context 的情况下处理 ThreadPool,也有可能在同一个线程上发生延续(例如,同一个池线程可能会启动并稍后为异步 I/O 操作的完成提供服务,巧合)。 @bN_,有:protected virtual Task OverridableMethod() return Task.Task.CompletedTask; 。注意async 不是虚拟方法签名的一部分,因此在派生类中重写此方法时仍然可以使用它。【参考方案6】:

我认为在使用 Task.Yield 时没有人提供真正的答案。 如果任务使用永无止境的循环(或冗长的同步作业),并且可能独占持有线程池线程(不允许其他任务使用此线程),则最需要它。如果在循环内代码同步运行,就会发生这种情况。 Task.Yield 将任务重新调度到线程池队列中,等待线程的其他任务可以执行。

示例:

  CancellationTokenSource cts;
  void Start()
  
        cts = new CancellationTokenSource();

        // run async operation
        var task = Task.Run(() => SomeWork(cts.Token), cts.Token);
        // wait for completion
        // after the completion handle the result/ cancellation/ errors
    

    async Task<int> SomeWork(CancellationToken cancellationToken)
    
        int result = 0;

        bool loopAgain = true;
        while (loopAgain)
        
            // do something ... means a substantial work or a micro batch here - not processing a single byte

            loopAgain = /* check for loop end && */  cancellationToken.IsCancellationRequested;
            if (loopAgain) 
                // reschedule  the task to the threadpool and free this thread for other waiting tasks
                await Task.Yield();
            
        
        cancellationToken.ThrowIfCancellationRequested();
        return result;
    

    void Cancel()
    
        // request cancelation
        cts.Cancel();
    

【讨论】:

再想一想,你可能可以这样使用它,但感觉不是一个好的设计:将一大堆 CPU 限制任务排队到线程池(超过它可以句柄),所以他们必须让步 Task.Yield() 给他们的同伴一个执行的机会。对于像这样的并发 CPU 绑定场景有更好的选择,例如Parallel 或 TPL Dataflow. @noseratio 如果循环中的代码执行用户定义的处理程序,则此模式的可能用法。代码不知道它的行为方式——它是异步的还是同步的。因此,作为预防措施,在循环结束时最好有一个 Task.Yield。这在消息处理总线中很常见。当总线实例化处理程序并处理消息时。如果它有很多并行运行的工人,那么它可以耗尽线程池线程。我在github.com/BBGONE/REBUS-TaskCoordinator 中使用了它来实现工人协调员 还有一件事 - 这可以被测试。我有一个 TaskCoordinator 测试实验室 github.com/BBGONE/TaskCoordinator 使用这种模式。如果不使用 Task.Yield,如果您配置为并行运行大量任务来处理消息,那么您将不会在控制台中显示消息 - 因为所有池线程都很忙,并且线程池不会添加更多线程 - 因为高 CPU 使用率。但是当使用 Task.Yield - 状态消息出现在控制台中。它每秒处理大约 200 000 条消息,并且当开销较小(无序列化)时 - 大约每秒 500 000 条。 我不认为在实现生产者/消费者模式的同时使用 Task.Yield 来克服 ThreadPool 饥饿是一个好主意。如果您想详细说明原因,我建议您另外提出一个问题。 @noseratio 听起来你已经起来了 :) ***.com/questions/53263258/…

以上是关于Task.Yield - 实际用途?的主要内容,如果未能解决你的问题,请参考以下文章

AtomicInteger 的实际用途

在用户界面和控制台应用程序中使用 Task.Yield() 的区别

JUnit 测试用例中“失败”的实际用途是啥?

ES6 WeakMap 的实际用途是啥?

jQuery.map - 该功能的实际用途?

ECMAScript 中 Atomics 对象的实际用途是啥?