Ответ 1
Ok Joe... как и было обещано, вот как вы можете в общем случае решить эту проблему с помощью пользовательского подкласса TaskScheduler
. Я протестировал эту реализацию, и она работает как шарм. Не забывайте, что вы не можете подключить отладчик, если хотите увидеть Application.ThreadException
, чтобы он действительно срабатывал!!!
Пользовательский TaskScheduler
Эта пользовательская реализация TaskScheduler привязана к определенному SynchronizationContext
при "рождении" и будет принимать каждый входящий Task
, который ему нужно выполнить, связать с ним продолжение Продолжение, которое будет срабатывать только в том случае, если логические ошибки 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 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
}
Некоторые примечания/отказ от этой реализации:
- Если вы используете конструктор без параметров, он подберет текущий SynchronizationContext... поэтому, если вы просто построите это в потоке WinForms (главный конструктор форм, независимо от того), и он будет работать автоматически. Бонус, у меня также есть конструктор, в котором вы можете явно передать в SynchronizationContext, который вы получили откуда-то еще.
- Я не представил реализацию TryExecuteTaskInline, поэтому эта реализация всегда будет просто стоять в очереди
Task
, над которой будет работать. Я оставляю это как упражнение для читателя. Это не сложно, просто... не нужно демонстрировать функциональность, о которой вы просите. - Я использую простой/примитивный подход к планированию/выполнению задач, которые используют ThreadPool. Есть определенно более богатые реализации, которые нужно иметь, но опять-таки фокус этой реализации - это просто исключение маршалинга из обращений к потоку "Приложение".
Хорошо, теперь у вас есть пара вариантов использования этого TaskScheduler:
Предварительная настройка экземпляра TaskFactory
Этот подход позволяет вам настроить TaskFactory
один раз, а затем любую задачу, с которой вы начинаете с того, что экземпляр factory будет использовать пользовательский TaskScheduler
. Это будет выглядеть примерно так:
При запуске приложения
private static readonly TaskFactory MyTaskFactory = new TaskFactory(new SynchronizationContextFaultPropagatingTaskScheduler());
Внутри кода
MyTaskFactory.StartNew(_ =>
{
// ... task impl here ...
});
Явный TaskScheduler для вызова
Другой подход состоит в том, чтобы просто создать экземпляр пользовательского 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);