为什么 TaskScheduler.Current 是默认的 TaskScheduler?
任务并行库非常棒,在过去的几个月里我经常使用它。然而,有件事确实困扰着我: 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
来缓解。 - 捕获调用堆栈会产生额外的性能成本。
- 当我确实希望一个任务依赖于另一个正在运行的父任务时,我更喜欢显式指定它以方便代码阅读,而不是依赖调用堆栈魔法。
我知道这个问题可能听起来很主观,但我找不到一个很好的客观论据来解释为什么这种行为是这样的。我确信我在这里遗漏了一些东西:这就是我向你求助的原因。
The Task Parallel Library is great and I've used it a lot in the past months. However, there's something really bothering me: the fact that TaskScheduler.Current
is the default task scheduler, not TaskScheduler.Default
. This is absolutely not obvious at first glance in the documentation nor samples.
Current
can lead to subtle bugs since its behavior is changing depending on whether you're inside another task. Which can't be determined easily.
Suppose I am writting a library of asynchronous methods, using the standard async pattern based on events to signal completion on the original synchronisation context, in the exact same way XxxAsync methods do in the .NET Framework (eg DownloadFileAsync
). I decide to use the Task Parallel Library for implementation because it's really easy to implement this behavior with the following code:
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());
}
}
So far, everything works well. Now, let's make a call to this library on a button click in a WPF or WinForms application:
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
//...
}
Here, the person writing the library call chose to start a new Task
when the operation completes. Nothing unusual. He or she follows examples found everywhere on the web and simply use Task.Factory.StartNew
without specifying the TaskScheduler
(and there is no easy overload to specify it at the second parameter). The DoSomethingElse
method works fine when called alone, but as soon at it's invoked by the event, the UI freezes since TaskFactory.Current
will reuse the synchronization context task scheduler from my library continuation.
Finding out this could take some time, especially if the second task call is buried down in some complex call stack. Of course, the fix here is simple once you know how everything works: always specify TaskScheduler.Default
for any operation you're expecting to be running on the thread pool. However, maybe the second task is started by another external library, not knowing about this behavior and naively using StartNew
without a specific scheduler. I'm expecting this case to be quite common.
After wrapping my head around it, I can't understand the choice of the team writing the TPL to use TaskScheduler.Current
instead of TaskScheduler.Default
as the default:
- It's not obvious at all,
Default
is not the default! And the documentation is seriously lacking. - The real task scheduler used by
Current
depends of the call stack! It's hard to maintain invariants with this behavior. - It's cumbersome to specify the task scheduler with
StartNew
since you have to specify the task creation options and cancellation token first, leading to long, less readable lines. This can be alleviated by writing an extension method or creating aTaskFactory
that usesDefault
. - Capturing the call stack has additional performance costs.
- When I really want a task to be dependent on another parent running task, I prefer to specify it explicitly to ease code reading rather than rely on call stack magic.
I know this question may sound quite subjective, but I can't find a good objective argument as to why this behavior is as it. I'm sure I'm missing something here: that's why I'm turning to you.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
我认为目前的行为是有道理的。如果我创建自己的任务计划程序,并启动一些启动其他任务的任务,我可能希望所有任务都使用我创建的计划程序。
我同意奇怪的是,有时从 UI 线程启动任务使用默认调度程序,有时则不使用。但我不知道如果我来设计它,我该如何让它变得更好。
关于您的具体问题:
TaskFactory.Create
可以为您推断类型。Dispatcher.Invoke()
而不是使用TaskScheduler.FromCurrentSynchronizationContext()
。I think the current behavior makes sense. If I create my own task scheduler, and start some task that starts other tasks, I probably want all the tasks to use the scheduler I created.
I agree that it's odd that sometimes starting a task from the UI thread uses the default scheduler and sometimes not. But I don't know how would I make this better if I was designing it.
Regarding your specific problems:
new Task(lambda).Start(scheduler)
. This has the disadvantage that you have to specify type argument if the task returns something.TaskFactory.Create
can infer the type for you.Dispatcher.Invoke()
instead of usingTaskScheduler.FromCurrentSynchronizationContext()
.[编辑]
以下仅解决
Task.Factory.StartNew
使用的调度程序的问题。但是,
Task.ContinueWith
有一个硬编码的TaskScheduler.Current
。[/编辑]
首先,有一个简单的解决方案可用 - 请参阅本文的底部。
这个问题背后的原因很简单:不仅有一个默认的任务调度程序(
TaskScheduler.Default
),而且还有一个TaskFactory
的默认任务调度程序(TaskFactory.Default
)。调度程序)。这个默认调度程序可以在创建时在
TaskFactory
的构造函数中指定。然而,
Task.Factory
后面的TaskFactory
是按如下方式创建的:正如你所看到的,没有指定
TaskScheduler
;null
用于默认构造函数 - 更好的是TaskScheduler.Default
(文档指出使用“Current”具有相同的结果)。这再次导致
TaskFactory.DefaultScheduler
(私有成员)的实现:在这里您应该能够识别出此行为的原因:由于 Task.Factory 没有默认的任务调度程序,因此当前的任务调度程序将被使用。
那么,当当前没有任务正在执行时(即我们没有当前的 TaskScheduler),为什么我们不会遇到
NullReferenceExceptions
呢?原因很简单:
TaskScheduler.Current
默认为TaskScheduler.Default
。我认为这是一个非常不幸的实施。
不过,有一个简单的修复方法:我们可以简单地将
Task.Factory
的默认TaskScheduler
设置为TaskScheduler.Default
我希望我能提供帮助我的回应虽然已经很晚了:-)
[EDIT]
The following only addresses the problem with the scheduler used by
Task.Factory.StartNew
.However,
Task.ContinueWith
has a hardcodedTaskScheduler.Current
.[/EDIT]
First, there is an easy solution available - see the bottom of this post.
The reason behind this problem is simple: There is not only a default task scheduler (
TaskScheduler.Default
) but also a default task scheduler for aTaskFactory
(TaskFactory.Scheduler
).This default scheduler can be specified in the constructor of the
TaskFactory
when it's created.However, the
TaskFactory
behindTask.Factory
is created as follows:As you can see, no
TaskScheduler
is specified;null
is used for the default constructor - better would beTaskScheduler.Default
(the documentation states that "Current" is used which has the same consequences).This again leads to the implementation of
TaskFactory.DefaultScheduler
(a private member):Here you should see be able to recognize the reason for this behaviour: As Task.Factory has no default task scheduler, the current one will be used.
So why don't we run into
NullReferenceExceptions
then, when no Task is currently executing (i.e. we have no current TaskScheduler)?The reason is simple:
TaskScheduler.Current
defaults toTaskScheduler.Default
.I would call this a very unfortunate implementation.
However, there is an easy fix available: We can simply set the default
TaskScheduler
ofTask.Factory
toTaskScheduler.Default
I hope I could help with my response although it's quite late :-)
) 而不是
Task.Factory.StartNew()
考虑使用:
Task.Run(
,这将始终在线程池线程上执行。我刚刚遇到了问题中描述的同样的问题,我认为这是处理这个问题的好方法。
请参阅此博客条目:
Task.Run 与 Task.Factory.StartNew
Instead of
Task.Factory.StartNew()
consider using:
Task.Run()
This will always execute on a thread pool thread. I just had the same problem described in the question and I think that is a good way of handling this.
See this blog entry:
Task.Run vs Task.Factory.StartNew
Default
是默认值,但并不总是Current
。正如其他人已经回答的那样,如果您希望任务在线程池上运行,则需要通过将
Default
调度程序传递到Current
调度程序到Current
调度程序来显式设置Current
调度程序>TaskFactory 或StartNew
方法。由于您的问题涉及一个库,所以我认为答案是您不应该做任何会更改库外部代码所看到的当前调度程序的事情。这意味着当您引发
SomeOperationCompleted
事件时,不应使用TaskScheduler.FromCurrentSynchronizationContext()
。相反,做这样的事情:我什至认为你不需要在
Default
调度程序上显式启动你的任务 - 让调用者确定Current
调度程序(如果他们愿意) 。Default
is the default, but it's not always theCurrent
.As others have already answered, if you want a task to run on the thread pool, you need to explicitly set the
Current
scheduler by passing theDefault
scheduler into either theTaskFactory
or theStartNew
method.Since your question involved a library though, I think the answer is that you should not do anything that will change the
Current
scheduler that's seen by code outside your library. That means that you should not useTaskScheduler.FromCurrentSynchronizationContext()
when you raise theSomeOperationCompleted
event. Instead, do something like this:I don't even think you need to explicitly start your task on the
Default
scheduler - let the caller determine theCurrent
scheduler if they want to.我刚刚花了几个小时尝试调试一个奇怪的问题,即我的任务被安排在 UI 线程上,即使我没有指定它。事实证明,问题正是您的示例代码所演示的:在 UI 线程上安排了一个任务延续,并且在该延续中的某个位置启动了一个新任务,然后将该任务安排在 UI 线程上,因为当前正在执行的任务有一个特定的
TaskScheduler
集。幸运的是,这些都是我拥有的代码,因此我可以通过确保我的代码在启动新任务时指定
TaskScheduler.Default
来修复它,但如果您没那么幸运,我的建议是使用 < code>Dispatcher.BeginInvoke 而不是使用 UI 调度程序。所以,而不是:
尝试:
虽然它的可读性有点差。
I've just spent hours trying to debug a weird issue where my task was scheduled on the UI thread, even though I didn't specify it to. It turned out the problem was exactly what your sample code demonstrated: A task continuation was scheduled on the UI thread, and somewhere in that continuation, a new task was started which then got scheduled on the UI thread, because the currently executing task had a specific
TaskScheduler
set.Luckily, it's all code I own, so I can fix it by making sure my code specify
TaskScheduler.Default
when starting new tasks, but if you aren't so lucky, my suggestion would be to useDispatcher.BeginInvoke
instead of using the UI scheduler.So, instead of:
Try:
It's a bit less readable though.