c# - 使用cancellationToken延迟后执行方法
问题描述
我正在使用 C#.net 核心在多人游戏环境中编写例程。
场景:如果玩家在特定时间内没有响应,则会发生超时,服务器代表玩家响应(player.autoPlay),然后游戏继续下一个玩家。为此,使用取消令牌引入了任务延迟。
当播放器在一定时间内实际响应时,token 取消延迟任务并发生异常,并避免运行 Player.AutoPlay()。
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 对负载/性能的影响是多少。每回合为每个用户创建一个任务。(我相信 TokenSource.Cancel 在每回合后也会销毁它)。因此,一次活动的任务是连接的用户数。
很高兴听到您的意见。
解决方案
我整理了一个示例应用程序,它使用Fallback和Timeout来实现所需的行为。
我为Player
and使用了以下虚拟类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);
关于这些政策的几点说明:
- 在 Timeout 的情况下,您可以在
TimeoutAsync<T>
方法调用中指定返回类型。 - 在 Fallback 的情况下,您可以在
Policy<T>
类级别指定返回类型。 - 即使
FallbackFlow
我们需要使用同步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
to传递NormalFlow
:
- 模拟播放器:
channelToNormalFlow.SetResult(new Dto {...});
- 正常流量:
var dto = await channelFromSimulator
而且我习惯于从以下CancellationTokenSource
位置停止:SimulatePlayer
NormalFlow
- 正常流量:
channelToSimulator.Cancel();
- 模拟播放器:
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
. timeoutPolicy 的令牌和我们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
推荐阅读
- android - 使用适用于 Android 的 Google Maps SDK 检测地理点是否位于折线内
- android - 如何从 Arraylist 在 whatsapp 中添加贴纸
- javascript - 在覆盖 div 中完成第一次单击时触发双击
- python - 保存在管理日志中时,Django unicode 转换卡住了?
- python - 使用 Pipenv 处理导入和 Pythonpath
- firebase - Firebase 中的事件转化
- python - 数据流:使用 python 管道更新 BigQuery 行
- python - 放置海龟设置的安全地方?
- angular - 无法读取未定义的属性“setParent”
- java - 上滑面板(Java 到 Kotlin)