使用 cancellationToken 延迟后执行方法
Execute a method after a delay with cancellationToken
我正在使用 C#.net core 在多人游戏环境中编写例程。
场景:如果玩家在特定时间内没有响应,则会发生超时,服务器会代表玩家响应 (player.autoPlay),然后游戏会转到下一位玩家。
为此,引入了带有取消标记的任务延迟。
当玩家在一定时间内真正响应with-时,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();
}
以上代码工作正常。
我的问题是,
- Is Task are the best way to achieve the objective or using Timer.start, OnTimedEvent & Timer.stop 会在这里发挥更好的作用。
- 这里把exception作为一个正常的逻辑来使用,我不太愿意去消化。有没有一种方法可以避免引发异常并仍然避免执行自动播放方法。
- 在可扩展性方面,当一百万用户连接时,load/performance 的命中率是多少,因为 Task.dalay。
每回合为每个用户创建一个任务。 (我相信 TokenSource.Cancel 也会在每一回合后摧毁它)。因此,一次的活动任务是连接的用户数。
很高兴听到您的意见。
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);
我整理了一个示例应用程序,它使用 Fallback and Timeout 来实现所需的行为。
我为 Player
和 Dto
使用了以下虚拟 classes:
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>
方法调用中指定 return 类型。
- 在回退的情况下,您可以在
Policy<T>
class 级别指定 return 类型。
- 即使
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
传递到 NormalFlow
:
- 模拟播放器:
channelToNormalFlow.SetResult(new Dto {...});
- 正常流:
var dto = await channelFromSimulator
我用 CancellationTokenSource
停止了 NormalFlow
的 SimulatePlayer
:
- 正常流:
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
是一个 combined/linked 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
我正在使用 C#.net core 在多人游戏环境中编写例程。
场景:如果玩家在特定时间内没有响应,则会发生超时,服务器会代表玩家响应 (player.autoPlay),然后游戏会转到下一位玩家。 为此,引入了带有取消标记的任务延迟。
当玩家在一定时间内真正响应with-时,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();
}
以上代码工作正常。
我的问题是,
- Is Task are the best way to achieve the objective or using Timer.start, OnTimedEvent & Timer.stop 会在这里发挥更好的作用。
- 这里把exception作为一个正常的逻辑来使用,我不太愿意去消化。有没有一种方法可以避免引发异常并仍然避免执行自动播放方法。
- 在可扩展性方面,当一百万用户连接时,load/performance 的命中率是多少,因为 Task.dalay。 每回合为每个用户创建一个任务。 (我相信 TokenSource.Cancel 也会在每一回合后摧毁它)。因此,一次的活动任务是连接的用户数。
很高兴听到您的意见。
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);
我整理了一个示例应用程序,它使用 Fallback and Timeout 来实现所需的行为。
我为 Player
和 Dto
使用了以下虚拟 classes:
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>
方法调用中指定 return 类型。 - 在回退的情况下,您可以在
Policy<T>
class 级别指定 return 类型。 - 即使
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
传递到 NormalFlow
:
- 模拟播放器:
channelToNormalFlow.SetResult(new Dto {...});
- 正常流:
var dto = await channelFromSimulator
我用 CancellationTokenSource
停止了 NormalFlow
的 SimulatePlayer
:
- 正常流:
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
是一个 combined/linkedCancellationToken
。 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