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().[编辑]以下仅解决了使用的调度程序的问题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.
我认为这是一个非常不幸的实施.
然而,可用一个简单的办法:我们可以简单的设置默认TaskScheduler的Task.Factory要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 });
Run Code Online (Sandbox Code Playgroud)
我希望我可以帮助我的回复虽然已经很晚了:-)
代替 Task.Factory.StartNew()
考虑使用: Task.Run()
这将始终在线程池线程上执行。我刚刚遇到问题中描述的相同问题,我认为这是处理此问题的一种好方法。
请参阅此博客条目:http : //blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
| 归档时间: |
|
| 查看次数: |
16648 次 |
| 最近记录: |