首页 > 解决方案 > 使用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();
}

上面的代码工作正常。

我的问题是,

  1. 任务是实现目标的最佳方式,还是使用 Timer.start、OnTimedEvent 和 Timer.stop 会在这里发挥更好的作用。
  2. 这里的异常用作正常逻辑,我不愿意消化。有没有办法可以避免引发异常并仍然避免自动播放方法执行。
  3. 在可扩展性方面,当一百万用户连接时,Task.dalay 对负载/性能的影响是多少。每回合为每个用户创建一个任务。(我相信 TokenSource.Cancel 在每回合后也会销毁它)。因此,一次活动的任务是连接的用户数。

很高兴听到您的意见。

标签: c#async-awaittimertask-parallel-libraryasp.net-core-3.1

解决方案


我整理了一个示例应用程序,它使用FallbackTimeout来实现所需的行为。

我为Playerand使用了以下虚拟类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将数据从SimulatePlayerto传递NormalFlow

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

而且我习惯于从以下CancellationTokenSource位置停止:SimulatePlayerNormalFlow

  • 正常流量: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通信NormalFlownormalFlowToSimulatorsimulatorToNormalFlow
  • ct是一个组合/链接的CancellationToken. timeoutPolicy 的令牌和我们normalFlowToSimulator的令牌。
  • NormalFlowSimulatePlayer.
  • 当他们两个都完成后,我检索结果。

模拟器及时响应时正常运行的输出:

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

推荐阅读