在TPL中,如果Task引发异常,则会捕获该异常并将其存储在Task.Exception中,然后遵循observed exceptions上的所有规则。如果它从未被观察过,它最终会在终结器线程上重新抛出并使进程崩溃。
有没有办法阻止Task捕获该异常,只是让它传播?
我感兴趣的任务已经在UI线程上运行(由TaskScheduler.FromCurrentSynchronizationContext提供),我希望异常能够逃脱,因此可以由我现有的Application.ThreadException处理程序处理。
我基本上希望Task中未处理的异常在按钮单击处理程序中表现得像未处理的异常:立即在UI线程上传播,并由ThreadException处理。
答案 0 :(得分:11)
Ok Joe ...正如所承诺的,以下是使用自定义TaskScheduler
子类来解决此问题的方法。我已经测试了这个实现,它就像一个魅力。 不要忘记如果您希望看到Application.ThreadException
实际开火,则无法附加调试器!
这个自定义的TaskScheduler实现在“诞生”时与特定的SynchronizationContext
相关联,并将获取它需要执行的每个传入Task
,将一个Continuation链接到它,只有在逻辑上才会触发Task
错误,当它触发时,Post
返回SynchronizationContext,它将从出现故障的Task
中抛出异常。
public sealed class SynchronizationContextFaultPropagatingTaskScheduler : TaskScheduler
{
#region Fields
private SynchronizationContext synchronizationContext;
private ConcurrentQueue<Task> taskQueue = new ConcurrentQueue<Task>();
#endregion
#region Constructors
public SynchronizationContextFaultPropagatingTaskScheduler() : this(SynchronizationContext.Current)
{
}
public SynchronizationContextFaultPropagatingTaskScheduler(SynchronizationContext synchronizationContext)
{
this.synchronizationContext = synchronizationContext;
}
#endregion
#region Base class overrides
protected override void QueueTask(Task task)
{
// Add a continuation to the task that will only execute if faulted and then post the exception back to the synchronization context
task.ContinueWith(antecedent =>
{
this.synchronizationContext.Post(sendState =>
{
throw (Exception)sendState;
},
antecedent.Exception);
},
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);
// Enqueue this task
this.taskQueue.Enqueue(task);
// Make sure we're processing all queued tasks
this.EnsureTasksAreBeingExecuted();
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
// Excercise for the reader
return false;
}
protected override IEnumerable<Task> GetScheduledTasks()
{
return this.taskQueue.ToArray();
}
#endregion
#region Helper methods
private void EnsureTasksAreBeingExecuted()
{
// Check if there's actually any tasks left at this point as it may have already been picked up by a previously executing thread pool thread (avoids queueing something up to the thread pool that will do nothing)
if(this.taskQueue.Count > 0)
{
ThreadPool.UnsafeQueueUserWorkItem(_ =>
{
Task nextTask;
// This thread pool thread will be used to drain the queue for as long as there are tasks in it
while(this.taskQueue.TryDequeue(out nextTask))
{
base.TryExecuteTask(nextTask);
}
},
null);
}
}
#endregion
}
有关此实施的一些说明/免责声明:
Task
排队等待处理。我把这作为读者的练习。这并不难,只是......没有必要展示你要求的功能。好的,现在您有几个使用此TaskScheduler的选项:
此方法允许您设置TaskFactory
一次,然后您使用该工厂实例启动的任何任务都将使用自定义TaskScheduler
。这基本上看起来像这样:
private static readonly TaskFactory MyTaskFactory = new TaskFactory(new SynchronizationContextFaultPropagatingTaskScheduler());
MyTaskFactory.StartNew(_ =>
{
// ... task impl here ...
});
另一种方法是创建自定义TaskScheduler
的实例,然后在每次启动任务时将其传递到默认StartNew
上的TaskFactory
。
private static readonly SynchronizationContextFaultPropagatingTaskScheduler MyFaultPropagatingTaskScheduler = new SynchronizationContextFaultPropagatingTaskScheduler();
Task.Factory.StartNew(_ =>
{
// ... task impl here ...
},
CancellationToken.None // your specific cancellationtoken here (if any)
TaskCreationOptions.None, // your proper options here
MyFaultPropagatingTaskScheduler);
答案 1 :(得分:4)
我找到了一种在某些时候能够充分发挥作用的解决方案。
var synchronizationContext = SynchronizationContext.Current;
var task = Task.Factory.StartNew(...);
task.ContinueWith(task =>
synchronizationContext.Post(state => {
if (!task.IsCanceled)
task.Wait();
}, null));
这会在UI线程上安排对task.Wait()
的调用。因为在我知道任务已经完成之前我没有执行Wait
,所以它实际上不会阻塞;它只会检查是否有异常,如果有,它会抛出。由于SynchronizationContext.Post
回调直接从消息循环执行(在Task
的上下文之外),因此TPL不会停止异常,并且它可以正常传播 - 就像它是一个按钮单击处理程序中的未处理异常。
另外一个问题是,如果任务被取消,我不想打电话给WaitAll
。如果你等待一个被取消的任务,TPL会抛出一个TaskCanceledException
,重新抛出是没有意义的。
在我的实际代码中,我有多个任务 - 初始任务和多个延续。如果其中任何一个(可能不止一个)获得异常,我想将AggregateException
传播回UI线程。以下是如何处理:
var synchronizationContext = SynchronizationContext.Current;
var firstTask = Task.Factory.StartNew(...);
var secondTask = firstTask.ContinueWith(...);
var thirdTask = secondTask.ContinueWith(...);
Task.Factory.ContinueWhenAll(
new[] { firstTask, secondTask, thirdTask },
tasks => synchronizationContext.Post(state =>
Task.WaitAll(tasks.Where(task => !task.IsCanceled).ToArray()), null));
相同的故事:完成所有任务后,在WaitAll
的上下文之外拨打Task
。它不会阻止,因为任务已经完成;如果任何任务出现故障,它只是一种简单的方法来抛出AggregateException
。
起初我担心,如果其中一个延续任务使用类似TaskContinuationOptions.OnlyOnRanToCompletion
的内容,并且第一个任务出现故障,那么WaitAll
调用可能会挂起(因为延续任务永远不会运行,并且我担心WaitAll
会阻止等待它运行)。但事实证明,TPL设计者比这更聪明 - 如果由于OnlyOn
或NotOn
标志而不能运行延续任务,那么延续任务将转换为Canceled
状态,所以它不会阻止WaitAll
。
当我使用多任务版本时,WaitAll
调用会引发AggregateException
,但AggregateException
无法通过ThreadException
处理程序:只有一个的内部异常传递给ThreadException
。因此,如果多个任务抛出异常,则只有其中一个到达线程异常处理程序。我不清楚为什么会这样,但我想弄明白。
答案 2 :(得分:0)
我没有办法让这些异常像主线程中的异常一样传播。为什么不挂钩同一个处理程序,你也挂钩Application.ThreadException
到TaskScheduler.UnobservedTaskException
?
答案 3 :(得分:0)
这样的事情适合吗?
public static async void Await(this Task task, Action action = null)
{
await task;
if (action != null)
action();
}
runningTask.Await();