通过并行调用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();
}
}
}
根据您的代码,如果两个线程同时访问
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标志的设置自动完成,锁,然后返回一个错误,模拟已经经历了其他线程检查和访问该标志,因为这将是一个自动操作,将一次完成一个。