大家好!我是长三月,一位在游戏行业工作多年的老程序员,专注于分享服务器开发相关的文章。
这周的文章因为工作繁忙又鸽了几天。本次的主题是降低同步的开销,是并发类别下的第二篇。
在游戏服务器开发中,多线程竞争资源时,为了保证资源状态的正确性,不可避免要使用加锁等同步方式。但加锁是有代价的,不仅会限制多线程以串行的方式运行,还会导致上下文切换,因此为了提升性能和可伸缩性,我们需要减少对锁的竞争。加锁对可伸缩性的影响取决于两个因素:一是单次锁占用的时间,二是请求锁的频率。我们可以从这两个角度对加锁进行优化。
减少锁竞争的第一种方法是缩小锁的范围。锁中代码的执行时间会直接影响程序的可伸缩性。假设一个独占锁内部的程序执行需要耗时20ms,那么每秒最多只能支持50次执行的并发量。因此需要将锁内部与同步无关的代码移到锁外,特别是执行耗时长且可能阻塞的操作,如网络IO。
让我们看这样的一个例子。游戏模式是开房间战斗,玩家可以自由进入或退出战斗房间,当房间人数满5人时自动开启战斗,当房间人数为0时房间自动解散,玩家进入或退出房间都会向房间中的其他人推送进入或退出的消息,玩家有每日可战斗次数,进入房间时消耗1次次数,退出房间时增加1次次数,该次数会计入DB。
为了保证房间状态的正确性,我们需要保证进入房间和退出房间的操作为原子操作,例如进入房间至少应包括:判断是否能进入,将玩家id加入到房间中的玩家集合,当玩家人数满5人时自动开启战斗等,这一系列操作应该组合为一个原子操作不可分割。原始的设计是编写joinRoom和exitRoom两个方法,并将锁加在这两个方法上,Java版代码如下:
synchronized int joinRoom(int playerId) {
// 将锁加在方法级别上
int errorCode = canJoin(playerId); // 先判断能否进入
if (errorCode != 0) {
return errorCode; // 不能进入则返回错误码
}
players.add(playerId); // 加入房间中玩家集合
if (players.size() >= 5) {
// 当玩家数量满5人时开启战斗
startFight();
}
notifyAll(playerId); // 将进入消息推送给房间中其他玩家
db.decreaseLeftFightNum(playerId); // 今日剩余可战斗次数减1,计入db
}
synchronized int exitRoom(playerId) {
int errorCode = canExit(playerId)
if (errorCode != 0) {
return errorCode
}
players.remove(playerId);
if (players.size() <= 0) {
destroyRoom();
}
notifyAll(playerId);
db.increaseLeftFightNum(playerId);
}
以上代码有可以优化的地方,优化方法是将notifyAll和对leftFightNum的修改移到锁以外。可以这样做的原因是,这两个操作本质是线程竞争完后做的后续跟随操作,无论它们是否放在同步块中,都不影响房间状态的正确性。另外,网络推送和数据库操作都有可能引发网络IO阻塞,因此不宜放在同步块中(尽管这两个操作都可以改成异步的,我们还是可以把它们移出去节省占用锁的时间)。修改后的代码如下:
int joinRoom(int<
优化游戏服务器:减少锁竞争与提升并发性能

文章讲述了在游戏服务器开发中如何通过缩小锁的范围、降低加锁频率、使用读写锁以及异步编程来优化同步操作,减少线程竞争,提升系统性能和可伸缩性。具体例子包括调整锁的位置以减少锁内代码执行时间,使用分段锁分散竞争,以及利用CAS和非阻塞队列实现更轻量级的同步。
最低0.47元/天 解锁文章
:降低同步的开销&spm=1001.2101.3001.5002&articleId=130145294&d=1&t=3&u=1a99962ea0c54b46ba6b262c66896ff9)
1220

被折叠的 条评论
为什么被折叠?



