为什么TaskScheduler.Current是默认的TaskScheduler?

Jul*_*ain 61 .net c# conceptual synchronizationcontext task-parallel-library

任务并行库很棒,在过去的几个月里我经常使用它.但是,有一些事情让我感到困扰:事实TaskScheduler.Current是默认的任务调度程序,而不是TaskScheduler.Default.这在文档和样本中乍一看绝对不是很明显.

Current可以导致细微的错误,因为它的行为正在改变,这取决于你是否在另一个任务中.哪个不容易确定.

假设我正在编写异步方法库,使用基于事件的标准异步模式来表示原始同步上下文的完成,这与XxxAsync方法在.NET Framework中完全相同(例如DownloadFileAsync).我决定使用任务并行库来实现,因为使用以下代码实现此行为非常容易:

public class MyLibrary
{
    public event EventHandler SomeOperationCompleted;

    private void OnSomeOperationCompleted()
    {
        SomeOperationCompleted?.Invoke(this, EventArgs.Empty);
    }

    public void DoSomeOperationAsync()
    {
        Task.Factory.StartNew(() =>
        {
            Thread.Sleep(1000); // simulate a long operation
        }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default)
        .ContinueWith(t =>
        {
            OnSomeOperationCompleted(); // trigger the event
        }, TaskScheduler.FromCurrentSynchronizationContext());
    }
}
Run Code Online (Sandbox Code Playgroud)

到目前为止,一切运作良好.现在,让我们在WPF或WinForms应用程序中单击按钮调用此库:

private void Button_OnClick(object sender, EventArgs args)
{
    var myLibrary = new MyLibrary();
    myLibrary.SomeOperationCompleted += (s, e) => DoSomethingElse();
    myLibrary.DoSomeOperationAsync(); // call that triggers the event asynchronously
}

private void DoSomethingElse() // the event handler
{
    //...
    Task.Factory.StartNew(() => Thread.Sleep(5000)); // simulate a long operation
    //...
}
Run Code Online (Sandbox Code Playgroud)

这里,编写库调用的人选择Task在操作完成时启动新的.没什么不寻常的.他或她遵循网络上随处可见的示例,只需使用Task.Factory.StartNew而不指定TaskScheduler(并且在第二个参数中没有容易的重载来指定它).DoSomethingElse单独调用时该方法可以正常工作,但是一旦事件调用它,UI TaskFactory.Current就会冻结,因为将重用我的库继续中的同步上下文任务调度程序.

找出这个可能需要一些时间,特别是如果第二个任务调用被埋没在一些复杂的调用堆栈中.当然,一旦你知道一切是如何工作的,这里的修复很简单:总是指定TaskScheduler.Default你期望在线程池上运行的任何操作.但是,第二个任务可能是由另一个外部库启动的,不了解这种行为,并且在StartNew没有特定调度程序的情况下天真地使用.我期待这种情况很常见.

在我的脑袋缠绕之后,我无法理解编写TPL的团队的选择,TaskScheduler.Current而不是TaskScheduler.Default默认:

  • 这根本不明显,Default不是默认的!文档严重缺乏.
  • 使用的真实任务调度程序Current取决于调用堆栈!使用此行为很难维护不变量.
  • 指定任务调度程序很麻烦,StartNew因为您必须首先指定任务创建选项和取消令牌,从而导致长且不易读的行.这可以通过编写扩展方法或创建TaskFactory使用来减轻Default.
  • 捕获调用堆栈会产生额外的性能成本.
  • 当我真的希望一个任务依赖于另一个父运行任务时,我更喜欢明确指定它以简化代码读取而不是依赖于调用堆栈魔术.

我知道这个问题可能听起来很主观,但我找不到一个好的客观论证,为什么这种行为就像它一样.我确定我在这里遗漏了一些东西:这就是我转向你的原因.

svi*_*ick 18

我认为目前的行为是有道理的.如果我创建自己的任务调度程序,并启动一些启动其他任务的任务,我可能希望所有任务都使用我创建的调度程序.

我同意奇怪的是,有时从UI线程启动任务使用默认调度程序,有时不使用.但我不知道如果我正在设计它,我将如何做得更好.

关于你的具体问题:

  • 我认为在指定的调度程序上启动新任务的最简单方法是new Task(lambda).Start(scheduler).这样做的缺点是,如果任务返回某些内容,则必须指定类型参数.TaskFactory.Create可以为你推断出类型.
  • 您可以使用Dispatcher.Invoke()而不是使用TaskScheduler.FromCurrentSynchronizationContext().

  • 我同意Julien,这种行为是糟糕的设计,并且在语义上将默认的含义改为api的一部分中的'default scheduler',但是'当前的调度程序,如果你在一个下运行,否则默认'在另一部分api正在寻找麻烦.实际上它也是rx团队的成功!http://social.msdn.microsoft.com/Forums/en-US/rx/thread/5a7fd5cf-073f-45d1-a4d4-93a80dac390a (3认同)
  • @Julien Lebosquain然后,您应该始终明确指定要在TPL调用中使用的TaskScheduler.输入的额外代码很少,但可以保证你得到你想要的东西. (2认同)

Mat*_*ias 7

[编辑]以下仅解决了使用的调度程序的问题Task.Factory.StartNew.
但是,Task.ContinueWith有一个硬编码TaskScheduler.Current.[/编辑]

首先,有一个简单的解决方案 - 请参阅本文的底部.

这个问题背后的原因很简单:不仅有一个默认的任务调度程序(TaskScheduler.Default),但也为默认的任务调度TaskFactory(TaskFactory.Scheduler).可以在TaskFactory创建它的构造函数中指定此默认调度程序.

但是,TaskFactory后面Task.Factory的创建如下:

s_factory = new TaskFactory();
Run Code Online (Sandbox Code Playgroud)

如您所见,没有TaskScheduler指定; null用于默认构造函数 - 更好的是TaskScheduler.Default(文档声明使用"当前"具有相同的结果).
这再次导致TaskFactory.DefaultScheduler(私人成员)的实施:

private TaskScheduler DefaultScheduler 
{ 
   get
   { 
      if (m_defaultScheduler == null) return TaskScheduler.Current;
      else return m_defaultScheduler;
   }
}
Run Code Online (Sandbox Code Playgroud)

在这里你应该看到能够识别出这种行为的原因:由于Task.Factory没有默认任务调度程序,因此将使用当前的任务调度程序.

那么,为什么我们不会遇到NullReferenceExceptions当前没有任务正在执行的任务(即我们当前没有TaskScheduler)?
原因很简单:

public static TaskScheduler Current
{
    get
    {
        Task internalCurrent = Task.InternalCurrent;
        if (internalCurrent != null)
        {
            return internalCurrent.ExecutingTaskScheduler;
        }
        return Default;
    }
}
Run Code Online (Sandbox Code Playgroud)

TaskScheduler.Current默认为TaskScheduler.Default.

我认为这是一个非常不幸的实施.

然而,可用一个简单的办法:我们可以简单的设置默认TaskSchedulerTask.FactoryTaskScheduler.Default

TaskFactory factory = Task.Factory;
factory.GetType().InvokeMember("m_defaultScheduler", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, null, factory, new object[] { TaskScheduler.Default });
Run Code Online (Sandbox Code Playgroud)

我希望我可以帮助我的回复虽然已经很晚了:-)


tes*_*ino 5

代替 Task.Factory.StartNew()

考虑使用: Task.Run()

这将始终在线程池线程上执行。我刚刚遇到问题中描述的相同问题,我认为这是处理此问题的一种好方法。

请参阅此博客条目:http : //blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx