任务并行库很棒,过去几个月我已经使用了很多。但是,确实有一些困扰我的地方: TaskScheduler.Current 是默认的任务计划程序,而不是 TaskScheduler.Default 。乍看之下,在文档或样本中这绝对是不明显的。
Current可能会导致细微的错误,因为它的行为会根据您是否在另一个任务中而改变。这不容易确定。

假设我正在编写一个异步方法库,它使用基于事件的标准异步模式来指示原始同步上下文上的完成,其方式与XxxAsync方法在.NET Framework中的执行方式完全相同(例如DownloadFileAsync)。我决定使用Task Parallel Library进行实现,因为使用以下代码来实现此行为确实很容易:

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

到目前为止,一切正常。现在,让我们在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
    //...
}

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

找出这一点可能要花费一些时间,特别是如果第二个任务调用被埋在某个复杂的调用堆栈中。当然,一旦您知道所有工作原理,此处的修复很简单:始终为希望在线程池上运行的任何操作指定TaskScheduler.Default。但是,也许第二个任务是由另一个外部库启动的,不知道此行为,而是在没有特定调度程序的情况下天真地使用StartNew。我希望这种情况很普遍。

把头缠住之后,我不明白编写TPL的团队选择使用TaskScheduler.Current而不是TaskScheduler.Default作为默认值的选择:
  • 一点也不明显,Default不是默认值!而且文件严重不足。
  • Current使用的实际任务调度程序取决于调用堆栈!这种行为很难保持不变。
  • StartNew指定任务调度程序很麻烦,因为您必须先指定任务创建选项和取消 token ,这会导致行长,可读性差。这可以通过编写扩展方法或创建使用TaskFactoryDefault来缓解。
  • 捕获调用堆栈会产生额外的性能成本。
  • 当我真的希望一个任务依赖于另一个正在运行的父任务时,我宁愿明确指定它以简化代码阅读,而不是依赖于调用堆栈魔术。

  • 我知道这个问题听上去很主观,但是我找不到一个很好的客观论据来说明这种行为为什么如此。我确定我在这里遗漏了一些东西:这就是为什么我转向你。

    最佳答案

    我认为目前的行为是有道理的。如果我创建自己的任务计划程序,并启动某个任务,再启动其他任务,则可能希望所有任务都使用我创建的计划程序。

    我同意,有时从UI线程启动任务使用默认调度程序而有时不使用默认调度程序是很奇怪的。但是我不知道如果我要设计它会怎样做。

    关于您的特定问题:

  • 我认为在指定的调度程序上启动新任务的最简单方法是new Task(lambda).Start(scheduler)。这样做的缺点是,如果任务返回某些内容,则必须指定type参数。 TaskFactory.Create可以为您推断类型。
  • 您可以使用Dispatcher.Invoke()代替TaskScheduler.FromCurrentSynchronizationContext()
  • 关于c# - 为什么TaskScheduler.Current是默认的TaskScheduler?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/6800705/

    10-11 02:00