Безопасное выполнение многократного Task <> в корпоративном приложении - PullRequest
2 голосов
/ 24 июня 2019

Я разрабатываю программную архитектуру для продукта, который может создавать серию «агентов», делающих некоторые полезные вещи. Допустим, каждый агент реализует интерфейс, имеющий функцию:

Task AsyncRun(CancellationToken token)

Потому что, поскольку эти агенты выполняют много операций ввода-вывода, может иметь смысл иметь функцию async. Более того, предполагается, что AsyncRun никогда не завершится, если не происходит исключений или явных отмен.

Теперь вопрос в том, что основная программа должна запускать это на нескольких агентах, я хотел бы знать правильный способ выполнения этой множественной задачи, сигнализировать о каждом отдельном завершении (которые связаны с отменой / ошибками): например, я думаю о чем-то вроде бесконечного цикла вроде этого

//.... all task cretaed are in the array tasks..
while(true)
{
     await Task.WhenAny(tasks)
     //.... check each single task for understand which one(s) exited
     // re-run the task if requested replacing in the array tasks
}

но не уверен, что это правильный (или даже лучший способ) И более того, я хотел бы знать, является ли это правильным шаблоном, особенно потому, что разработчик может не соответствовать RunAsync и выполнять блокирующий вызов, и в этом случае все приложение будет зависать.

Ответы [ 2 ]

0 голосов
/ 24 июня 2019

// повторное выполнение задачи, если требуется замена в массиве задач

Это первое, что я хотел бы изменить. Гораздо лучше , а не , чтобы приложение обрабатывало свой собственный "перезапуск". Если операция завершилась неудачно, нет гарантии, что приложение сможет восстановиться. Это верно для любых операций на любом языке / во время выполнения.

Лучшее решение состоит в том, чтобы позволить другому приложению перезапустить это. Разрешить распространение исключения (регистрация его, если это возможно), и разрешить ему завершить приложение. Затем при необходимости перезапустите процесс «менеджер» (буквально отдельный исполняемый процесс). Именно так работают все современные системы высокой доступности, от диспетчера служб Win32 до ASP.NET, диспетчера контейнеров Kubernetes и среды выполнения функций Azure.

Обратите внимание, что если вы хотите пойти по этому пути, возможно, имеет смысл разделить задачи на разные процессы, чтобы они могли быть перезапущены независимо. Таким образом, перезапуск в одном не вызовет перезапуск в других.

Однако, если вы хотите сохранить все свои задачи в одном процессе, то у вас есть подходящее решение. Если у вас есть известное количество задач в начале процесса, и это число не изменится (если они не будут выполнены), вы можете немного упростить код, исключив перезапуск и используя Task.WhenAll вместо Task.WhenAny:

async Task RunAsync(Func<CancellationToken, Task> work, CancellationToken token)
{
  while (true)
  {
    try { await work(token); }
    catch
    {
      // log...
    }

    if (we-should-not-restart)
      break;
  }
}

List<Func<CancellationToken, Task>> workToDo = ...;
var tasks = workToDo.Select(work => RunAsync(work, token));
await Task.WhenAll(tasks);
// Only gets here if they all complete/fail and were not restarted.

разработчик может не выполнить RunAsync и выполнить блокирующий вызов, в этом случае все приложение будет зависать.

Лучший способ предотвратить это - заключить вызов в Task.Run, вот так:

await work(token);

становится таким:

await Task.Run(() => work(token));
0 голосов
/ 24 июня 2019

Чтобы узнать, успешно ли выполнено задание, отменено или не выполнено, вы можете использовать продолжение.Продолжение будет вызвано, как только задача завершится, будь то из-за сбоя, отмены или завершения.:

using (var tokenSource = new CancellationTokenSource())
{
    IEnumerable<IAgent> agents; // TODO: initialize

    var tasks = new List<Task>();
    foreach (var agent in agents)
    {
        var task = agent.RunAsync(tokenSource.Token)
            .ContinueWith(t =>
            {
                if (t.IsCanceled)
                {
                    // Do something if cancelled.
                }
                else if (t.IsFaulted)
                {
                    // Do something if faulted (with t.Exception)
                }
                else
                {
                    // Do something if the task has completed.
                }
            });

        tasks.Add(task);
    }

    await Task.WhenAll(tasks);
}

В конце вы будете ждать продолжения задач.См. Также этот ответ .

Если вы боитесь, что реализации IAgent создадут блокирующие вызовы, и хотите предотвратить зависание приложения, вы можете заключить вызов в асинхронный метод вTask.Run.Таким образом, вызов агента выполняется в пуле потоков и поэтому не блокируется:

var task = Task.Run(async () =>
    await agent.RunAsync(tokenSource.Token)
        .ContinueWith(t =>
        {
            // Same as above
        }));

Вы можете использовать Task.Factory.StartNew вместо того, чтобы пометить задачу как longrunning, например.

...