通过并行调用WCF避免相同的代码运行两次

本文关键字:运行 代码 两次 调用 并行 WCF | 更新日期: 2023-09-27 17:53:02

我正在制作一个小游戏,用户使用WCF发送移动,当所有移动都发送到游戏中模拟下一轮。

数据存储在MSSQL数据库中,通过EntityFramework访问。

在每次调用结束时检查是否所有移动都已发送并且应该开始模拟。

这里有一个问题。如果两个玩家都在一个非常短的时间窗口内发送他们的移动,那么两个线程都有可能检查是否所有的移动都已发送,发现它们已经发送,并在其中一个足够远的地方设置模拟状态(并阻止任何进一步的尝试)之前开始两次模拟。

为了补救这一点,我写了以下检查。

总体思路是这样的:

  • 一个线程检查是否所有的移动都被接收,如果他们是尝试移动到重复检查
  • 检查游戏是否处于正确状态,如果是,在数据库的游戏行上设置一个唯一的向导。
  • 等待x毫秒(我已将其设置为100)
  • 检查游戏中是否仍然设置了相同的向导
  • 如果是,继续前进,如果不退出,其他人已经完成了一半的逻辑,当他们到达这里时会模拟。

我需要一些输入,如果有任何回路漏洞,或者如何改进。

    private void TryToRunSimulationRound(GWDatabase db, GameModel game)
    {
        // if we simulate, this is going to be our id while doing it
        Guid simulatorGuid = Guid.NewGuid();
        // force reload of game record
        db.Entry(game).Reload();
        // is game in right state, and have all moves been received? if so kick it in to gear!
        if (game.GameState == GameState.ActiveWaitingForMoves &&  game.Players.Any(x => x.NextMoves == null || x.NextMoves.Count() == 0) == false)
        {
            // set simulation id
            game.SimulatorGuid = simulatorGuid;
            game.GameState = GameState.ActiveWaitingForSimulation;
            db.SaveChanges();
            // wait to see if someone else might be simulating
            Thread.Sleep(100);
            // get a new copy of the game to check against
            db.Entry(game).Reload();
            // if we still have same guid on the simulator then we can go ahead.. otherwise stop, someone else is running.
            // this will allow the later thread to do the simulation while the earlier thread will skip it.
            if (simulatorGuid == game.SimulatorGuid && game.GameState == GameState.ActiveWaitingForSimulation)
            {
                var s = new SimulationHandler();
                s.RunSimulation(db, game.Id);
                game.SimulatorGuid = null;
                // wait a little in the end just to make sure we dont have any slow threads just getting to the check...
                Thread.Sleep(100); 
                db.SaveChanges();
            }
            else
            {
                GeneralHelpers.AddLog(db, game, null, GameLogType.Debug, "Duplicate simulations stopped, game: " + game.Id);
            }
        }

注:这个错误花了我很长时间才弄清楚,直到我运行了一个有5000个查询的System.Threading.Tasks.Parallel.ForEach,我才能每次都重现它。这种情况可能永远不会在现实世界中发生,但这是一个爱好项目,并且很有趣的玩弄;)

这是Juan回答后更新的代码。这也阻止了错误的发生,似乎是一个更干净的解决方案。

    private static readonly object _SimulationLock = new object();
    private void TryToRunSimulationRound(GWDatabase db, GameModel game)
    {
        bool runSimulation = false;
        lock (_SimulationLock) //Ensure that only 1 thread checks to run at a time.
        {
            // force reload of game record
            db.Entry(game).Reload();
            if (game.GameState == GameState.ActiveWaitingForMoves 
                && game.Players.Any(x => x.NextMoves == null || x.NextMoves.Count() == 0) == false)
            {
                game.GameState = GameState.ActiveWaitingForSimulation;
                db.SaveChanges();
                runSimulation = true;
            }
        }
        if(runSimulation){
            // get a new copy of the game to check against
            db.Entry(game).Reload();
            if (game.GameState == GameState.ActiveWaitingForSimulation)
            {
                var s = new SimulationHandler();
                s.RunSimulation(db, game.Id);
                db.SaveChanges();
            }
        }
    }

通过并行调用WCF避免相同的代码运行两次

根据您的代码,如果两个线程同时访问

if (game.GameState == GameState.ActiveWaitingForMoves &&  game.Players.Any(x => x.NextMoves == null || x.NextMoves.Count() == 0) == false)

两者都将尝试设置模拟ID,并并发地操作状态,这是不好的。

game.SimulatorGuid = simulatorGuid; //thread 1 is setting this, while t2 is doing that too.
game.GameState = GameState.ActiveWaitingForSimulation;
db.SaveChanges(); //which thread wins?

有一个非常好的模式来防止这些竞争条件,而不需要使用process.sleep。如果您只需要对数据库进行原子访问(atomic确保所有操作都按此顺序完成,没有竞争条件或其他线程的干扰),它还可以防止需要用模糊的条件填充代码。

这个问题可以通过跨线程共享一个静态对象来解决,并使用内置的锁机制来强制执行原子操作:
private static readonly object SimulationLock= new object();

,然后用锁定安全预防措施包围代码,

`private void AtomicRunSimulationRound(GWDatabase db, GameModel game)
{
    lock(SimulationLock) //Ensure that only 1 thread has access to the method at once
    {
        TryToRunSimulationRound(db, game);
    }
}
private void TryToRunSimulationRound(GWDatabase db, GameModel game)
{
    //Thread.sleep is not needed anymore
}`

有更优雅的解决方案。而不是等待资源被释放,我宁愿有游戏检查和ActiveWaitingForSimulation标志的设置自动完成,锁,然后返回一个错误,模拟已经经历了其他线程检查和访问该标志,因为这将是一个自动操作,将一次完成一个。