#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();
}
Приведенный выше код работает нормально.
Мой вопрос здесь,
- Является ли задача лучшим способом достижения цели или использование Timer.start, OnTimedEvent и Timer.stop сыграли бы здесь лучшую роль.
- Здесь исключение используется как обычная логика, которую я не хочу переваривать. Есть ли способ избежать возникновения исключения и при этом избежать выполнения метода автозапуска.
- С точки зрения масштабируемости, какова нагрузка / производительность из-за 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 уже выполняет проверку.