【问题标题】:Avoiding same code running twice with parallel calls to WCF通过对 WCF 的并行调用避免相同的代码运行两次
【发布时间】:2015-06-03 21:50:35
【问题描述】:

我正在制作一个小游戏,其中用户使用 WCF 发送动作,当游戏中的所有动作都发送完毕后,会模拟下一轮。

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

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

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

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

大致思路是这样的:

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

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

    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,我每次都可以重现它。这种情况在现实世界中可能永远不会发生,但这是一个业余项目,玩起来很有趣;)

更新 这是胡安回答后的更新代码。这也阻止了错误的发生,并且似乎是一个更清洁的解决方案。

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

【问题讨论】:

    标签: c# multithreading entity-framework wcf


    【解决方案1】:

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

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

    【讨论】:

    • 根据您的建议进行了一些更改,它似乎有效,在我接受之前进行了一些测试。
    • 我有点担心 lock() 会阻塞系统。我之前的方法在最坏的情况下将线程阻塞了 200 毫秒,而锁可以迫使不幸的线程无限期地等待。
    • 锁定整个方法绝对会使系统陷入瘫痪,但仅锁定竞争条件(即“检查并设置”变量)将是一种非常快速且合理的方法。
    猜你喜欢
    • 2013-06-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-21
    • 1970-01-01
    • 2012-06-11
    • 2018-03-28
    • 1970-01-01
    相关资源
    最近更新 更多