Как я могу позволить исключениям Задачи распространяться обратно в поток пользовательского интерфейса? - PullRequest
10 голосов
/ 17 декабря 2011

В TPL, если Task выдает исключение, это исключение захватывается и сохраняется в Task.Exception , а затем следует всем правилам наблюдаемых исключений.Если он никогда не наблюдается, он в конечном итоге перебрасывается в поток финализатора и завершает работу процесса.

Есть ли способ не дать Задаче перехватить это исключение и просто позволить ему распространяться вместо этого?

Задача, которая меня интересует, уже будет выполняться в потоке пользовательского интерфейса (любезно предоставлено TaskScheduler.FromCurrentSynchronizationContext ), и я хочу, чтобы исключение сбрасывалось, чтобы оно могло быть обработано моим существующим Application.ThreadException обработчик.

Я в основном хочу, чтобы необработанные исключения в Задаче вели себя как необработанные исключения в обработчике нажатия кнопки: немедленно распространялись в потоке пользовательского интерфейса и обрабатывались исключением ThreadException.

Ответы [ 4 ]

11 голосов
/ 20 декабря 2011

Хорошо, Джо ... как и было обещано, вот как вы можете в общих чертах решить эту проблему с помощью пользовательского подкласса 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'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
}

Некоторые примечания / отказ от ответственности в этой реализации:

  • Если вы используете конструктор без параметров, он подхватит текущий SynchronizationContext ... так что, если вы просто создадите его в потоке WinForms (главный конструктор формы, что угодно), и он будет работать автоматически.Кроме того, у меня также есть конструктор, в котором вы можете явно передать SynchronizationContext, который вы получили откуда-то еще.
  • Я не предоставил реализацию TryExecuteTaskInline, поэтому эта реализация просто всегда будет ставить в очередь Task, чтобы бытьработал над.Я оставляю это как упражнение для читателя.Это не сложно, просто ... не обязательно демонстрировать требуемую функциональность.
  • Я использую простой / примитивный подход к планированию / выполнению задач, использующих ThreadPool.Определенно существуют более богатые реализации, но опять-таки в центре внимания этой реализации лежит просто маршалинг исключений обратно в поток «Application»

Хорошо, теперь у вас есть несколько вариантов использования этого TaskScheduler:

Предварительная настройка экземпляра TaskFactory

Этот подход позволяет настроить TaskFactory один раз, и тогда любая задача, которую вы запускаете с этим экземпляром фабрики, будет использовать пользовательский TaskScheduler.По сути, это будет выглядеть примерно так:

При запуске приложения

private static readonly TaskFactory MyTaskFactory = new TaskFactory(new SynchronizationContextFaultPropagatingTaskScheduler());

Во всем коде

MyTaskFactory.StartNew(_ =>
{
    // ... task impl here ...
});

Явный TaskScheduler Per-Call

Другой подходэто просто создать экземпляр пользовательского 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);
4 голосов
/ 19 декабря 2011

Я нашел решение, которое иногда работает адекватно.

Отдельная задача

var synchronizationContext = SynchronizationContext.Current;
var task = Task.Factory.StartNew(...);

task.ContinueWith(task =>
    synchronizationContext.Post(state => {
        if (!task.IsCanceled)
            task.Wait();
    }, null));

Это запланирует вызов на task.Wait() в потоке пользовательского интерфейса.Поскольку я не выполняю Wait, пока не узнаю, что задача уже выполнена, она фактически не будет блокироваться;он просто проверит, было ли исключение, и если да, то выдаст.Поскольку обратный вызов SynchronizationContext.Post выполняется прямо из цикла сообщений (вне контекста Task), TPL не остановит исключение и может распространяться как обычно, как если бы это было необработанное исключение вобработчик нажатия кнопки.

Еще одна проблема - я не хочу звонить WaitAll, если задача была отменена.Если вы ожидаете отмененную задачу, TPL выдает TaskCanceledException, которую нет смысла перебрасывать.

Несколько задач

В моем реальном коде у меня есть несколько задач -начальная задача и несколько продолжений.Если какой-либо из них (потенциально более одного) получает исключение, я хочу передать AggregateException обратно в поток пользовательского интерфейса.Вот как с этим справиться:

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 передается только одно его внутренних исключений.Поэтому, если несколько задач выдавали исключения, только одна из них достигает обработчика потока-исключения.Я не понимаю, почему это так, но я пытаюсь понять это.

0 голосов
/ 08 мая 2013

Что-то типа этого костюма?

0 голосов
/ 17 декабря 2011

Я не знаю, чтобы эти исключения распространялись как исключения из основного потока. Почему бы просто не подключить тот же обработчик, к которому вы подключаете Application.ThreadException к TaskScheduler.UnobservedTaskException?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...