Task.Yield - 真实用法?

时间:2014-05-02 15:22:21

标签: c# multithreading async-await .net-4.5 c#-5.0

我一直在阅读有关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我无法打开任务( 是的我知道我实际上可以使用Web worker )。但是使用c#我可以

如果是这样 - 为什么在我可以释放它的时候甚至会不时发布?

修改

添加参考:

来自hereenter image description here

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

enter image description here

6 个答案:

答案 0 :(得分:55)

当你看到:

await Task.Yield();

你可以这样思考:

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

所有这一切都确保延续将在未来异步发生。通过异步我的意思是执行控件将返回到async方法的调用者,并且延续回调将发生在同一堆栈帧上。< / p>

什么时候完全取决于调用者线程的同步上下文。

对于UI线程,继续将在消息循环的某个未来迭代中发生,由Application.RunWinForms)或Dispatcher.Run运行({ {3}})。在内部,它归结为Win32 PostMessage API,它将自定义消息发布到UI线程的消息队列。当此消息被泵送和处理时,将调用await延续回调。你究竟何时会发生这种情况,你完全失控。

此外,Windows有自己的优先级来提取消息:WPF。最相关的部分:

  

根据该计划,优先级可以被视为三级。所有   发布的消息优先于用户输入消息,因为   他们居住在不同的队列中。并且所有用户输入消息都是   优先级高于WM_PAINT和WM_TIMER消息。

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

这与您在问题中提到的JavaScript setTimer类比有所不同。在浏览器的消息泵处理了所有用户输入消息后,{em}将调用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;
}

INFO: Window Message Priorities,对于在UI线程上运行的此类后台操作的每次迭代,您不超过50毫秒。

对于没有同步上下文的非UI线程await Task.Yield()只是将连续切换为随机池线程。无法保证它将成为当前线程的不同的线程,它只能保证是异步延续。如果ThreadPool正在挨饿,它可能会将延续计划到同一个线程上。

在ASP.NET 中,除It is recommended中提到的解决方法外,执行await Task.Yield()根本没有意义。否则,它只会损害使用冗余线程切换的Web应用程序性能。

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

关于您引用的图书,在我看来,使用Task.Yield的方法是错误的。我解释了为什么他们在上面的UI线程中出错了。对于非UI池线程,除了运行像@StephenCleary's answer这样的自定义任务泵之外,线程中没有&#34;其他任务执行&#34;

更新以回复评论:

  

...如何进行异步操作并保持在同一个线程中   ?..

举个简单的例子:WinForms app:

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

Form_Load将返回调用者(WinFroms框架代码已触发Load事件),然后消息框将异步显示,在将来迭代的消息循环运行{ {1}}。延续回调与Application.Run()排队,后者在内部将私有Windows消息发布到UI线程的消息循环。当此消息被泵送时,将在同一线程上执行回调。

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

答案 1 :(得分:17)

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

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

答案 2 :(得分:6)

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

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

  

“大多数UI中UI线程上存在的同步上下文   环境通常会优先考虑发布到上下文的工作   比输入和渲染工作。因此,不要依赖等待   Task.Yield();保持用户界面的响应。“

参考:MSDN: Task.Yield Method

答案 3 :(得分:2)

首先让我澄清一下: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上的mono 4.5上产生的输出,其他设置的结果可能会有所不同

如果您在方法之上移动Task.Yield,它将从头开始异步,并且不会阻止调用线程。

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

答案 4 :(得分:0)

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

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

答案 5 :(得分:0)

我认为在使用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();
    }