Дождаться завершения всех задач вне зависимости от исключени
От: Слава  
Дата: 29.12.19 04:04
Оценка:
Здравствуйте.

Положим, у меня есть ненадёжный сервис, и я хочу получить от него некие данные. У меня есть класс:

private class CrapResult
{
    public string Crap1;
    public bool IsCrap1Received;
    public string Crap2;
    public bool IsCrap2Received;
    public string Crap3;
    public bool IsCrap3Received;
}


Я пытаюсь получить все эти значения, чтобы передать их дальше. Если получить не удаётся, я сохраняю то, что получено, и в следующий раз не запрашиваю, а наступает этот следующий раз после некоей паузы, неважно какой.

Получение делается примерно так:
var tasks = new List<Task>();
Task<string> crap1Task = null;

if (!res.IsCrap1Received)
{
    crap1Task = crapService.GetCrap1(session, id,...);
    tasks.Add(crap1Task);
}
...
await Task.WhenAll(tasks);

if (crap1Task!=null)
{
    res.IsCrap1Received = true;
    res.Crap1 = crap1Task.Result;
}
...


При этом, если хоть одна задача из списка tasks выбросит исключение, то и await Task.WhenAll(tasks) моментально прекратит работу и выбросит AggregateException, не дожидаясь, пока остальные задачи выполнятся. Но мне-то надо дождаться всех! Что мне, писать свой собственный метод, который при выбросе исключения будет убирать задачу в состоянии faulted из списка (ещё остаётся вопрос, как её найти), и ещё раз вызовет WhenAll, пока все задачи не завершатся успешно или не выбросят исключение? А неужели такой метод никто не написал до меня? Я знаю про существование метода WaitAll, но он блокирующий и мне как-то не хочется его вызывать из async-метода, или из обычного метода, который выше по стеку вызывает async-метод.

Я чувствую у себя некий пробел в знаниях, но сформулировать его не могу.
Отредактировано 29.12.2019 4:11 Слава . Предыдущая версия . Еще …
Отредактировано 29.12.2019 4:10 Слава . Предыдущая версия .
забыл как программировать
Re: Дождаться завершения всех задач вне зависимости от исклю
От: takTak  
Дата: 29.12.19 05:14
Оценка:
что-то я сомневаюсь, что WhenAll сразу же прекратит исполнение, если один из Task бросит исключение, я думаю, что WhenAll будет пытаться выполнить все Tasks независимо от каких-то исключений,
попробуй вот так:


async Task Main()
{
    var task = Task.WhenAll(A(), B());

    try
    {
        var results = await task;
        Console.WriteLine(results);
    }
    catch (Exception)
    {
        if (task.Exception != null)
        {
            throw task.Exception;
        }
    }
}


кроме того, если надо, то при желании легко прилепить какую-нибудь Retry Policy (https://stackoverflow.com/questions/1563191/cleanest-way-to-write-retry-logic, https://github.com/App-vNext/Polly)
Отредактировано 29.12.2019 5:20 takTak . Предыдущая версия .
Re: Дождаться завершения всех задач вне зависимости от исключени
От: Passerby  
Дата: 29.12.19 05:41
Оценка:
Сначала обрабатывать исключения самому. Если набор строк не может содержать какой-либо строки (null или абракадабру или "-1" и т.д., при обработке исключения возвращать эту строку. Если возможны любые строки, заменить Tаsk<string> на Tаsk<(string,bool)> (по bool определять, есть ошибка или нет) или на Tаsk<(string,int)> (по int определять, есть ошибка или нет, если есть какая)или на Tаsk<string[]> (строка [1] — описание ошибки, если она есть, если нет =null).
Re: Дождаться завершения всех задач вне зависимости от исключени
От: romangr Россия  
Дата: 29.12.19 10:15
Оценка:
Здравствуйте, Слава, Вы писали:

Можно обернуть задачу, которую возвращает Task.WhenAll в свою, и await'ить ее.
Например, как-то так:
class Program
{
    static async Task Main(string[] args)
    {
        var tasks = new Task[10];
        var random = new Random();
        for (int i = 0; i < tasks.Length; i++)
        {
            tasks[i] = Task.Factory.StartNew(() =>
            {
                var n = random.Next(1, 5);
                if (n < 2)
                {
                    throw new InvalidOperationException();
                }
                return n;
            });
        }
        var task = Task.WhenAll(tasks).ContinueWith((t) =>
        {
            return t.Status == TaskStatus.RanToCompletion;
        });
        var success = await task;
        if (success)
        {
            Console.WriteLine("All tasks succeded");
        }
        else
        {
            for (int i = 0; i < tasks.Length; i++)
            {
                if (tasks[i].Status != TaskStatus.RanToCompletion)
                {
                    Console.WriteLine($"Task {i} failed.");
                }
            }
        }
    }
}
... << RSDN@Home 1.3.110 alpha 5 rev. 62>>
Re: Дождаться завершения всех задач вне зависимости от исключени
От: Ночной Смотрящий Россия  
Дата: 29.12.19 13:14
Оценка: 7 (2)
Здравствуйте, Слава, Вы писали:

С>Я чувствую у себя некий пробел в знаниях, но сформулировать его не могу.


Может я чего не понял в твоей задаче, но:
async (TResult result, Exception exception) Guard<TResult>(this Task<TResult> task)
{
  try
  {
    return (await task, null);
  }
  catch (Exception ex)
  {
    return (default(TResult), ex);
  }
}

...

var res = await Task.WhenAll(tasks.Select(t => t.Guard());
... << RSDN@Home 1.3.17 alpha 5 rev. 62>>
Re: Дождаться завершения всех задач вне зависимости от исключени
От: Passerby  
Дата: 30.12.19 02:35
Оценка:
Здравствуйте, Слава, Вы писали:
С>var tasks = new List<Task>();
С>Task<string> crap1Task = null;

Проконсультируйте по синтаксису, правильно так, как написано или в таком случае:
var tasks = new List<Task<string>>();

Или оба варианта правильны?
Re: Дождаться завершения всех задач вне зависимости от исклю
От: fmiracle  
Дата: 30.12.19 07:38
Оценка: 76 (1)
Здравствуйте, Слава, Вы писали:

С>При этом, если хоть одна задача из списка tasks выбросит исключение, то и await Task.WhenAll(tasks) моментально прекратит работу и выбросит AggregateException, не дожидаясь, пока остальные задачи выполнятся. Но мне-то надо дождаться всех! Что мне, писать свой собственный метод, который при выбросе исключения будет убирать задачу в состоянии faulted из списка (ещё остаётся вопрос, как её найти), и ещё раз вызовет WhenAll, пока все задачи не завершатся успешно или не выбросят исключение? А неужели такой метод никто не написал до меня? Я знаю про существование метода WaitAll, но он блокирующий и мне как-то не хочется его вызывать из async-метода, или из обычного метода, который выше по стеку вызывает async-метод.


С>Я чувствую у себя некий пробел в знаниях, но сформулировать его не могу.


В той постановке как задача описана, я бы предложил решение через собственную обертку так:
async Task GetAllData( ICrapService crapService, string session, int id )
{
    var res = new CrapResult();

    await Task.WhenAll(
      RetrieveAndStoreData( crapService.GetCrap1( session, id ), ( string s ) => res.Crap1 = s, () => res.IsCrap1Received = true ),
      RetrieveAndStoreData( crapService.GetCrap2( session, id ), ( string s ) => res.Crap2 = s, () => res.IsCrap2Received = true ),
      RetrieveAndStoreData( crapService.GetCrap3( session, id ), ( string s ) => res.Crap3 = s, () => res.IsCrap3Received = true ),
    );
}

async Task RetrieveAndStoreData<T>( Task<T> getData, Action<T> setData, Action setFlag )
{
    try
    {
        var res = await getData;
        setData( res );
        setFlag();
    }
    catch(Exception ex)
    {
        // подавляем, как  я понял, или что там еще надо сделать для случая "была проблема при загрузке, надо попробовать позже". 
    }
}


Но я не уверен, что это хорошо в целом. Порефакторить для начала на этом уровне можно так, избавившись от отдельных флажков (которые могут разойтись с реальными данными):
private class ImprovedCrapResult
{
    public class BasicCrapResult<T>
    {
        public T Result { get; set; }
        // возможно еще параметры типа успешно/неуспешно, или когда была последняя попытка запроса.
    }
    public BasicCrapResult<string> Crap1; // null - нет результата, иначе - собственно результат. Или по отдельным полям в BasicCrapResult
    public BasicCrapResult<string> Crap2;
    public BasicCrapResult<string> Crap3;
}


А далее надо смотреть выше, нужен ли вообще такой вот своеобразный класс результата и обобщенный запрос всего что на самом деле вместе-то и не нужно, раз может подождать.
А если оно все равно нужно вместе, просто это будут новые попытки, так их стоит вынести в загрузку отдельных частей — чтобы каждая из них пробовала несколько раз с паузами, а потом или возвращала все же результат или уже выкидывала исключение что "все, дальше пытаться бесполезно" — и это будет общая ошибка. И запускать их в параллельс обычным WhenAll.
Отредактировано 01.01.2020 17:04 fmiracle . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.