Выполнить метод после задержки с помощью CancellationToken

#c# #async-await #timer #task-parallel-library #asp.net-core-3.1

#c# #асинхронный-ожидание #таймер #задача-параллельная-библиотека #asp.net-core-3.1

Вопрос:

Я пишу процедуру в многопользовательской игровой среде, используя C#.net ядро.

Сценарий: если в случае, если игрок не отвечает в течение определенного времени, наступает тайм-аут, и сервер отвечает от имени игрока (player.AutoPlay), и игра переходит к следующему игроку. Для этого задержка задачи вводится с помощью токена отмены.

Когда player фактически отвечает -в определенное время, токен отменяет задачу задержки, и возникает исключение, и это позволяет избежать запуска Player.Автозапуск().

 public CancellationTokenSource TokenSource { get; private set; } = new CancellationTokenSource();

public async Task CreateFallbackforPlayerTurn(string msg, Player player)
{
    var fakeDto = new dto{value = "something"};
    try
        {
            await Task.Delay(DefaultTimeout, TokenSource.Token);
            var resp = player.AutoPlay(fakeDto);
            OnPlayerResponse(resp, true);
        }
        catch (OperationCanceledException ex)
        {
            Console.WriteLine($"fallback canceled as player responded, { ex.Message}");
        }
}


public async Task ActionFromClient(Dto actualResponse)
{
    OnPlayerResponseactualResponse, false);
}


public void OnPlayerResponse(Dto dto, bool fromAutoPlayer = false)
{
  if (fromAutoPlayer == false)
  {
     TokenSource.Cancel();
  }
  ProcessResponse();
}
  

Приведенный выше код работает нормально.

Мой вопрос здесь,

  1. Является ли задача лучшим способом достижения цели или использование Timer.start, OnTimedEvent и Timer.stop сыграли бы здесь лучшую роль.
  2. Здесь исключение используется как обычная логика, которую я не хочу переваривать. Есть ли способ избежать возникновения исключения и при этом избежать выполнения метода автозапуска.
  3. С точки зрения масштабируемости, какова нагрузка / производительность из-за Task.dalay при подключении миллиона пользователей. Для каждого пользователя создается одна задача на каждом ходу. (Я считаю, что токен-источник.Отмена также уничтожает его после каждого хода). Таким образом, активные задачи одновременно — это количество подключенных пользователей.

Рад услышать ваши мнения.

Комментарии:

1. Под капотом [Задача. Delay] использует таймер, как показано в исходном коде. имхо, нет ничего плохого в использовании задачи для этого.

2. @KumarShishir Вы рассматривали возможность использования политики резервного копирования и тайм- аута Polly ? Ваша логика ожидания может быть обернута в политику тайм-аута, и в случае сбоя ваша резервная логика может вызвать автоматическое воспроизведение.

3. @PeterBons Это решает мою первую проблему, спасибо

4. @PeterCsala Мне начал нравиться этот подход. Помимо резервного копирования и тайм-аута, в нем есть больше опций, таких как waitRetry и circuitBreaker. У вас есть несколько примеров? Я обнаружил трудности с настройкой резервной политики. AsyncTimeoutPolicy TimeoutPolicy = Политика. TimeoutAsync(30, стратегия тайм-аута. Оптимистично); var fallbackPolicy = Policy.xxx(OnPlayerResponse(fakeDto, true)); var policyWrap1 = Политика. WrapAsync(резервная политика, политика ожидания); переменная policyWrap2 = Резервная политика. Перенос (политика тайм-аута);

5. Я честно думаю, что Polly — это просто абстракция, которая скрывает таймеры и обработку OperationCanceledException , поэтому с точки зрения исходного вопроса это не сильно улучшило бы существующий код. Тем не менее, Polly по-прежнему является отличной библиотекой.

Ответ №1:

Я собрал пример приложения, которое использует резервное копирование и тайм-аут для достижения желаемого поведения.

Я использовал следующие фиктивные классы для Player и Dto :

 public class Dto
{
    public string Value { get; set; }
}

public class Player
{
    public Dto AutoPlay(Dto dto)
    {
        Console.WriteLine($"{nameof(AutoPlay)} method has been called.");
        return dto;
    }

    public Dto Play(Dto dto)
    {
        Console.WriteLine($"{nameof(Play)} method has been called.");
        return dto;
    }
}
  

Определения политики выглядят следующим образом:

 const int TimeoutInSec = 10;
var player = new Player();

var timeout = Policy
    .TimeoutAsync<Dto>(TimeSpan.FromSeconds(TimeoutInSec));

var fallback = Policy<Dto>
    .Handle<TaskCanceledException>()
    .Or<TimeoutRejectedException>()
    .FallbackAsync(_ => Task.FromResult(FallbackFlow(player)));

var strategy = Policy.WrapAsync(fallback, timeout);
  

Всего пара замечаний относительно этих политик:

  • В случае тайм-аута вы можете указать возвращаемый тип при TimeoutAsync<T> вызове метода.
  • В случае резервного копирования вы можете указать возвращаемый тип на уровне Policy<T> класса.
  • Даже несмотря FallbackFlow на то, что синхронизация is нам нужна FallBackAsync , чтобы иметь возможность подключать резервную политику к политике тайм-аута (внутри WrapAsync ).
  • Эта резервная политика будет обрабатывать сбой политики тайм-аута ( TimeoutRejectedException ) и CancellationToken отмененное исключение ( TaskCanceledException ).

Определение FallbackFlow этого простого:

 public static Dto FallbackFlow(Player player)
{
    Console.WriteLine($"{nameof(FallbackFlow)} has been called.");
    return player.AutoPlay(new Dto { Value = "fallback" });
}
  

Определение NormalFlow может быть намного проще, чем у меня. Я использовал это, потому что мне нужно было создать симулятор пользовательского ввода (через случайный промежуток времени он ответит).

 public static async Task<Dto> NormalFlow(Player player, CancellationToken timeoutPolicyToken, 
    Task<Dto> channelFromSimulator, CancellationTokenSource channelToSimulator)
{
    Console.WriteLine($"{nameof(NormalFlow)} has been called.");
    await Task.WhenAny(channelFromSimulator, Task.Delay(1000000, timeoutPolicyToken));
    
    if (!channelFromSimulator.IsCompletedSuccessfully)
    {
        Console.WriteLine($"{nameof(NormalFlow)} has been canceled");
        channelToSimulator.Cancel();
        timeoutPolicyToken.ThrowIfCancellationRequested();
    }

    var dto = await channelFromSimulator;
    Console.WriteLine($"{nameof(NormalFlow)} has received user data.");
    return player.Play(dto);
}
  

Пара замечаний относительно этой реализации:

  • Используется Task.WhenAny для ожидания либо ввода пользователем channelFromSimulator , либо запуска политики тайм-аута Task.Delay(1000000, timeoutPolicyToken)
    • Если срабатывает политика тайм-аута, то timeoutPolicyToken она будет отменена
  • Когда сработает политика тайм-аута, другое задание завершится неудачей, так channelFromSimulator.IsCompletedSuccessfull и будет false .
    • Мы должны уведомить симулятор о прекращении работы: channelToSimulator.Cancel();
    • мы должны уведомить нашу стратегию, чтобы перевести проблему в резервную политику: timeoutPolicyToken.ThrowIfCancellationRequested();
  • Если тайм-аут не сработал, мы извлекаем информацию из симулятора: var dto = await channelFromSimulator;

Реализация симулятора выглядит следующим образом:

 public static async Task SimulatePlayer(TaskCompletionSource<Dto> channelToNormalFlow, 
    CancellationToken channelFromNormalFlow)
{
    var rand = new Random();
    var userResponseInSec = rand.Next() % 20;
    Console.WriteLine($"Simulator will respond in {userResponseInSec} seconds");
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(userResponseInSec), channelFromNormalFlow);
    }
    catch (TaskCanceledException)
    {
        Console.WriteLine("Simulator has been canceled");
        return;
    }
    Console.WriteLine("Simulator is about to respond");
    channelToNormalFlow.SetResult(new Dto { Value = "user provided data" });
}
  

Пара замечаний относительно реализации:

  • Task.Delay Будет либо ждать случайное количество секунд, либо пока симулятор не будет отменен channelFromNormalFlow
  • Если он отменен, он автоматически завершится
  • Если он не отменен, то он создаст некоторые фиктивные данные и отправит их в NormalFlow : channelToNormalFlow.SetResult .

Как вы можете видеть, я использовал TaskCompletionSource для передачи данных из SimulatePlayer в NormalFlow :

  • SimulatePlayer: channelToNormalFlow.SetResult(new Dto {...});
  • Нормальный поток: var dto = await channelFromSimulator

И я использовал CancellationTokenSource , чтобы остановить SimulatePlayer из NormalFlow :

  • Нормальный поток: channelToSimulator.Cancel();
  • SimulatePlayer: Task.Delay(TimeSpan.FromSeconds(userResponseInSec), channelFromNormalFlow)

Наконец, давайте соберем все эти части вместе:

 var normalFlowToSimulator = new CancellationTokenSource();
var simulatorToNormalFlow = new TaskCompletionSource<Dto>();
var theJob = strategy.ExecuteAsync(async (ct) => await NormalFlow(player, ct, simulatorToNormalFlow.Task, normalFlowToSimulator), normalFlowToSimulator.Token);

await Task.WhenAll(theJob, SimulatePlayer(simulatorToNormalFlow, normalFlowToSimulator.Token));
var response = await theJob;

Console.WriteLine($"Result: {response.Value}");
  

Всего пара замечаний:

  • Как я уже сказал, я привык к разным объектам для обработки связи между SimulatePlayer и NormalFlow : normalFlowToSimulator , simulatorToNormalFlow
  • ct является комбинированным / связанным CancellationToken . Токен политики тайм-аута и наш normalFlowToSimulator токен.
  • Я запускаю NormalFlow (внутри устойчивой стратегии) параллельно с SimulatePlayer .
  • Когда оба они завершены, я получаю результат.

Вывод нормального запуска, когда симулятор отвечает вовремя:

 NormalFlow has been called.
Simulator will respond in 4 seconds
Simulator is about to respond
NormalFlow has received user data.
Play method has been called.
Result: user provided data
  

Вывод резервного запуска, когда симулятор не отвечает вовремя:

 NormalFlow has been called.
Simulator will respond in 14 seconds
NormalFlow has been canceled
Simulator has been canceled
FallbackFlow has been called.
AutoPlay method has been called.
Result: fallback
  

Ответ №2:

   var source = new CancellationTokenSource();  
              CancellationToken token = source.Token;  
              Task.Factory.StartNew(() => {   
                for(int i=0;i< 10000;i  )  
                {  
                    Console.WriteLine(i);  
                    if (token.IsCancellationRequested)  
                        token.ThrowIfCancellationRequested();  
                }  
              }, token);  
              source.CancelAfter(1000);  
  

Комментарии:

1. Вы проверяли это? Я почти уверен, что задача завершится до того, как будет запущена отмена.

2. Не делайте этого: 1) В соответствии с документацией Task.Run предпочтительнее, чем Task. Завод.Начать заново. 2) Всегда ожидайте выполнения ваших задач. 3) Ваша проверка if не нужна, поскольку ThrowIfCancellationRequested уже выполняет проверку.