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

Avoiding same code running twice with parallel calls to WCF

我正在制作一个小游戏,用户可以在其中使用 WCF 发送动作,并且在游戏中发送所有动作后模拟下一轮。

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

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

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

为了解决这个问题,我写了以下检查。

大意是这样的:

我需要一些关于这是否有任何循环漏洞或如何改进的意见。

    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);
            }
        }

P.S。这个错误花了我很长时间才弄清楚,直到我 运行 一个带有 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();
            }
        }
    }

根据您的代码,如果在这种情况下两个线程同时访问:

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。如果您只需要对数据库进行原子访问(原子确保所有操作按该顺序完成,没有竞争条件或来自其他线程的干扰),它还可以避免用模糊条件填充代码的需要。

可以通过跨线程共享静态对象并使用强制执行原子操作的内置锁定机制来解决:

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 标志的设置,然后返回一个错误,表明其他线程已经在进行模拟检查和访问那个标志,因为那将是一个原子操作,并且一次完成一个。