任务并行库很棒,我在过去的几个月里用了很多。然而,有一件事真的困扰着我:TaskScheduler.Current
是默认的任务调度程序,而不是TaskScheduler.Default
。乍一看,这在文档和示例中都是不明显的。
Current
可能会导致细微的错误,因为它的行为会根据您是否在另一个任务中而发生变化。这是不容易确定的。
假设我正在编写一个异步方法库,使用基于事件的标准异步模式在原始同步上下文中发出完成信号,就像XxxAsync方法在.NET框架中所做的那样(例如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
。没什么特别的。他或她遵循web上随处可见的示例,简单地使用Task.Factory.StartNew
,而不指定TaskScheduler
(而且在第二个参数中指定它也不容易重载)。单独调用DoSomethingElse
方法时,它工作得很好,但一旦它被事件调用,UI就会冻结,因为TaskFactory.Current
将重用我的库context中的同步上下文任务调度程序。
找出这一点可能需要一些时间,特别是如果第二个任务调用隐藏在一些复杂的调用堆栈中。当然,一旦您了解了所有操作的工作原理,这里的修复方法就很简单:总是为您希望在线程池上运行的任何操作指定TaskScheduler.Default
。然而,也许第二个任务是由另一个外部库启动的,它不知道这种行为,并且在没有特定调度程序的情况下天真地使用StartNew
。我希望这种情况很常见。
考虑到这一点,我无法理解编写第三方公共关系的团队为什么选择使用TaskScheduler.Current
而不是TaskScheduler.Default
作为默认设置:
Default
不是默认的!Current
使用的真正的任务调度程序StartNew
指定任务调度器很难使用这个behavior.Default
.TaskFactory
来缓解。调用堆栈有额外的性能成本。我知道这个问题可能听起来很主观,但我找不到一个很好的客观论据来解释为什么这种行为是这样的。我确信我在这里遗漏了什么:这就是我求助于你的原因。
发布于 2011-07-23 22:22:23
我认为目前的行为是有道理的。如果我创建了自己的任务调度程序,并启动了一些启动其他任务的任务,我可能希望所有任务都使用我创建的调度程序。
我同意,从UI线程启动任务有时使用默认调度程序,有时不使用,这很奇怪。但我不知道如果我在设计它,我该如何让它变得更好。
关于你的具体问题:
new Task(lambda).Start(scheduler)
。这样做的缺点是,如果任务返回某些内容,则必须指定类型参数。TaskFactory.Create
可以为你推断类型。Dispatcher.Invoke()
代替发布于 2011-11-20 21:39:40
编辑以下内容仅解决Task.Factory.StartNew
使用的调度程序的问题。
但是,Task.ContinueWith
有一个硬编码的TaskScheduler.Current
。/EDIT
首先,有一个简单的解决方案--参见这篇文章的底部。
这个问题背后的原因很简单:不仅有一个默认的任务调度器(TaskScheduler.Default
),而且还有一个TaskFactory
的默认任务调度器(TaskFactory.Scheduler
)。这个默认调度程序可以在创建时在TaskFactory
的构造函数中指定。
但是,Task.Factory
背后的TaskFactory
是这样创建的:
s_factory = new TaskFactory();
正如您所看到的,没有指定TaskScheduler
;null
被用作默认构造函数-最好是TaskScheduler.Default
(文档中指出使用"Current“具有相同的后果)。
这再次导致了TaskFactory.DefaultScheduler
(私有成员)的实现:
private TaskScheduler DefaultScheduler
{
get
{
if (m_defaultScheduler == null) return TaskScheduler.Current;
else return m_defaultScheduler;
}
}
在这里,您应该看到能够识别此行为的原因:由于Task.Factory没有默认的任务调度程序,因此将使用当前的任务调度程序。
那么,当当前没有任务正在执行时(即我们没有当前的TaskScheduler),我们为什么不运行NullReferenceExceptions
呢?
原因很简单:
public static TaskScheduler Current
{
get
{
Task internalCurrent = Task.InternalCurrent;
if (internalCurrent != null)
{
return internalCurrent.ExecutingTaskScheduler;
}
return Default;
}
}
TaskScheduler.Current
默认为TaskScheduler.Default
。
我会称这是一个非常不幸的实现。
但是,有一个简单的修复方法:我们可以简单地将Task.Factory
的默认TaskScheduler
设置为TaskScheduler.Default
TaskFactory factory = Task.Factory;
factory.GetType().InvokeMember("m_defaultScheduler", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, null, factory, new object[] { TaskScheduler.Default });
我希望我能帮助我的回应,尽管现在已经很晚了:-)
发布于 2013-08-28 15:40:08
代替Task.Factory.StartNew()
考虑使用:Task.Run()
这将始终在线程池线程上执行。我刚刚遇到了问题中描述的相同问题,我认为这是一个很好的处理方法。
查看此博客条目:http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
https://stackoverflow.com/questions/6800705
复制相似问题