使用 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();
}

以上代码工作正常。

我的问题是,

  1. Is Task are the best way to achieve the objective or using Timer.start, OnTimedEvent & Timer.stop 会在这里发挥更好的作用。
  2. 这里把exception作为一个正常的逻辑来使用,我不太愿意去消化。有没有一种方法可以避免引发异常并仍然避免执行自动播放方法。
  3. 在可扩展性方面,当一百万用户连接时,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 来实现所需的行为。

我为 PlayerDto 使用了以下虚拟 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
  • 如果它被取消那么它将静默退出
  • 如果没有取消,那么它会产生一些虚拟数据并将其发送到 NormalFlowchannelToNormalFlow.SetResult

如您所见,我使用 TaskCompletionSource 将数据从 SimulatePlayer 传递到 NormalFlow:

  • 模拟播放器:channelToNormalFlow.SetResult(new Dto {...});
  • 正常流:var dto = await channelFromSimulator

我用 CancellationTokenSource 停止了 NormalFlowSimulatePlayer:

  • 正常流: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}");

几个注意事项:

  • 正如我所说,我习惯于使用不同的对象来处理 SimulatePlayerNormalFlow 之间的通信:normalFlowToSimulatorsimulatorToNormalFlow
  • 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