Получение состояния задачи при исключении - PullRequest
0 голосов
/ 09 декабря 2018

Я создаю приложение, которое должно выполнять много параллельных задач.Эти задачи содержат несколько вызовов различных асинхронных методов (в основном это запросы к некоторым API REST с использованием HttpClient) с некоторой промежуточной обработкой, и я действительно не хочу перехватывать исключения внутри самой задачи.Вместо этого я бы предпочел сделать это, ожидая их, используя методы WhenAny / WhenAll.При получении исключения мне также необходимо собрать некоторые исходные данные этой задачи для последующего анализа (скажем, записать их в журнал).

Есть свойство Task.AsyncState, которое кажется идеальным контейнером для этих данных.Итак, я передаю объект TaskState при создании задачи следующим образом:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            new Program().RunTasksAsync();
            Console.ReadLine();
        }

        class TaskState
        {
            public int MyValue { get; }

            public TaskState(int myValue)
            {
                MyValue = myValue;
            }
        }

        private async Task<int> AsyncMethod1(int value)
        {
            await Task.Delay(500);
            return value + 1;
        }

        private async Task<int> AsyncMethod2(int value)
        {
            await Task.Delay(500);

            if (value % 3 == 0)
                throw new Exception("Expected exception");
            else
                return value * 2;
        }

        private async Task DoJobAsync(object state)
        {
            var taskState = state as TaskState;

            var i1 = await AsyncMethod1(taskState.MyValue);
            var i2 = await AsyncMethod2(i1);

            Console.WriteLine($"The result is {i2}");
        }

        private async void RunTasksAsync()
        {
            const int tasksCount = 10;
            var tasks = new List<Task>(tasksCount);
            var taskFactory = new TaskFactory();

            for (var i = 0; i < tasksCount; i++)
            {
                var state = new TaskState(i);
                var task = taskFactory.StartNew(async state => await DoJobAsync(state), state).Unwrap();
                tasks.Add(task);
            }

            while (tasks.Count > 0) {
                var task = await Task.WhenAny(tasks);
                tasks.Remove(task);
                var state = task.AsyncState as TaskState;

                if (task.Status == TaskStatus.Faulted)
                    Console.WriteLine($"Caught an exception while executing task, task data: {state?.MyValue}");
            }

            Console.WriteLine("All tasks finished");
        }
    }
}

Проблема с кодом выше заключается в том, что Unwrap () создает прокси-сервер задачи, который не имеет значения AsyncState, он всегда равен нулю, поэтомукогда дело доходит до исключения, я не могу получить исходное состояние задачи.Если я удаляю модификаторы Unwrap () и async / await в делегате, метод WaitAny () завершается до фактического завершения задачи.

Если я использую Task.Run(() => DoJobAsync(state)) вместо TaskFactory.StartNew (), tasksзакончить в правильном порядке, но таким образом я не могу установить AsyncState при создании задач.

Конечно, я могу использовать Dictionary<Task, TaskState> для хранения параметров задач в случае сбоя, но мне это кажется немного грязным, и, по сравнению с использованием свойства AsyncState, для поиска потребуется времядля совпадения в этой коллекции в случае большого количества одновременных задач.

Может кто-нибудь предложить более элегантное решение в этом случае?Есть ли другой способ получить состояние задачи при исключении?

Ответы [ 2 ]

0 голосов
/ 10 декабря 2018

Надлежащее место для включения информации о том, что выполняла операция, для ведения журнала, когда она завершилась неудачей, - , за исключением того, что вы выбросили .Не заставляйте абонента отслеживать не только задачу, но и кучу информации об этой задаче, чтобы они могли надлежащим образом обрабатывать ошибки.

public class ExpectedException : Exception
{
    public int Value { get; }
    public ExpectedException(int value, string message) : base(message)
    {
        Value = value;
    }
}
private async Task<int> AsyncMethod1(int value)
{
    await Task.Delay(500);
    return value + 1;
}
private async Task<int> AsyncMethod2(int value)
{
    await Task.Delay(500);

    if (value % 3 == 0)
        throw new ExpectedException(value, "Expected exception");
    else
        return value * 2;
}
private async Task DoJobAsync(int value)
{
    var i1 = await AsyncMethod1(value);
    var i2 = await AsyncMethod2(i1);

    Console.WriteLine($"The result is {i2}");
}
private async Task RunTasksAsync()
{
    const int tasksCount = 10;

    var tasks = Enumerable.Range(0, tasksCount)
        .Select(async i =>
        {
            try
            {
                await DoJobAsync(i);
            }
            catch (ExpectedException exception)
            {
                Console.WriteLine($"Caught an exception while executing task, task data: {exception.Value}");
            }
        })
        .ToList();

    await Task.WhenAll(tasks);

    Console.WriteLine("All tasks finished");
}

Если исключение выдается в вашемреальный код не генерируется явно, а вместо этого генерируется кодом, который вы не контролируете, тогда вам может понадобиться перехватить эти исключения и обернуть их в собственное новое исключение (то есть потребуется принять внутреннее исключение ипередать его в базовый конструктор).Просто запустите метод.Вам не нужно запускать уже асинхронную операцию нового потока пула потоков (если только он не был написан неправильно с самого начала).Если вы хотите запустить некоторый код при сбое задачи , используйте попытку catch , а не пытайтесь делать то, что вы делали.Это добавляет много сложности и намного больше места для ошибок.

Стоит отметить, что из-за того, как я рефакторинг кода обработки ошибок, исходные входные данные все еще находятся в области видимости, поэтому включение их висключение даже не требуется, но я оставляю его там, потому что возможно, что код обработки ошибок будет дальше по стеку вызовов, или что информация для печати - это больше, чем просто ввод для функции.Но если ни один из них не выполняется, вы можете просто использовать входные данные в блоке catch, потому что он все еще находится в области видимости.

0 голосов
/ 10 декабря 2018

Элегантное решение для вас:

var task = DoJobAsync(state);

Таким образом, вы передаете свои данные, и порядок выполнения правильный.

...