任务并行库很棒,我在过去几个月中经常使用它。然而,有一件事情真的困扰着我:TaskScheduler.Current
是默认任务调度程序,而不是 TaskScheduler.Default
。这在文档和示例中一眼看上去绝对不明显。
Current
可能会导致微妙的错误,因为其行为取决于是否在另一个任务中。这很难确定。
假设我正在编写一个异步方法库,使用基于事件的标准异步模式,在原同步上下文中使用事件来信号完成,就像 .NET Framework 中的 XxxAsync 方法一样(例如 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());
}
}
到目前为止,一切都运行良好。现在,在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
指定任务调度器很麻烦,因为你必须先指定任务创建选项和取消令牌,导致代码行变长、可读性降低。可以通过编写扩展方法或创建一个使用Default
的TaskFactory
来缓解这种情况。 - 捕获调用堆栈会增加额外的性能成本。
- 当我真正希望一个任务依赖于另一个正在运行的父任务时,我更喜欢明确指定它以方便代码阅读,而不是依赖于调用堆栈魔法。
我知道这个问题听起来可能相当主观,但我找不到一个好的客观论据来说明为什么会出现这种行为。我相信我在这里漏掉了什么:这就是为什么我求助于你。
DoSomethingElse
),它假设它将在UI上下文中调用吗?(如果这是您试图提出的观点-即它正在创建不在UI上下文中的任务) - Damien_The_UnbelieverDoSomethingElse
可以在此处的任何上下文中运行,但在这种特定情况下,它创建的任务将在父任务的上下文中运行,在 UI 线程上运行,而无需知道。如果使用了“Default”任务调度程序,则没有问题。我没有问题指定它,但我不能控制每个第三方库,也不总是意识到这一事实。我真正不理解的是为什么Current
是默认值,而所有这些潜在危险的上下文都可能发生变化。然而,这个问题可能太具有争议性了。 - Julien Lebosquain