构建和同步多线程游戏循环
Posted
技术标签:
【中文标题】构建和同步多线程游戏循环【英文标题】:Structuring and Synchronizing a Multithreaded Game Loop 【发布时间】:2012-10-02 05:09:02 【问题描述】:关于我的游戏循环的线程安全,我遇到了一个轻微的难题。我下面有 3 个线程(包括主线程),它们可以一起工作。一个用于事件管理(主线程),一个用于逻辑,一个用于渲染。所有这 3 个线程都存在于它们自己的类中,如下所示。在基本测试中,该结构可以正常工作。该系统使用 SFML 并使用 OpenGL 进行渲染。
int main()
Gamestate gs;
EventManager em(&gs);
LogicManager lm(&gs);
Renderer renderer(&gs);
lm.start();
renderer.start();
em.eventLoop();
return 0;
但是,您可能已经注意到,我有一个“Gamestate”类,它旨在充当需要在线程之间共享的所有资源的容器(大多数情况下,LogicManager 作为编写器,Renderer 作为读取器。 EventManager 主要用于窗口事件)。我的问题是:(1 和 2 是最重要的)
1) 这是处理事情的好方法吗?意思是使用“全局” Gamestate 类是个好主意吗?有没有更好的办法?
2) 我的意图是让 Gamestate 在 getter/setter 中具有互斥锁,但它不适用于读取,因为我无法在对象仍处于锁定状态时返回它,这意味着我必须将同步放在外面的 getter/setter 并公开互斥锁。这也意味着我将为所有不同的资源拥有大量的互斥锁。解决这个问题的最优雅的方法是什么?
3) 我让所有线程都访问“bool run”以检查是否继续它们的循环
while(gs->run)
....
如果我在 EventManager 中收到退出消息,run 会设置为 false。我是否需要同步该变量?我可以将其设置为 volatile 吗?
4) 不断取消引用指针等是否会对性能产生影响?例如 gs->objects->entitylist.at(2)->move();做所有那些'->'和'。导致任何重大放缓?
【问题讨论】:
【参考方案1】:全局状态
1) 这是处理事情的好方法吗?意思是使用“全局” Gamestate 类是个好主意吗?有没有更好的办法?
对于游戏,相对于一些可重用的代码,我想说全局状态就足够了。您甚至可以避免传递游戏状态指针,而是真正将其设为全局变量。
同步
2) 我的意图是让 Gamestate 在 getter/setter 中具有互斥锁,但它不适用于读取,因为我无法在对象仍处于锁定状态时返回它,这意味着我必须将同步放在外面的 getter/setter 并公开互斥锁。这也意味着我将为所有不同的资源拥有大量的互斥锁。解决这个问题的最优雅的方法是什么?
我会尝试从交易的角度来考虑这一点。将每个状态更改包装到自己的互斥锁代码中不仅会影响性能,而且如果代码获取一个状态元素,对其执行一些计算并稍后设置值,而其他一些代码修改了之间的相同元素。所以我会尝试构建LogicManager
和Renderer
,使与Gamestate 的所有交互都捆绑在几个地方。在该交互期间,线程应在状态上持有一个互斥锁。
如果您想强制使用互斥锁,那么您可以创建一些至少有两个类的构造。我们称他们为GameStateData
和GameStateAccess
。 GameStateData
将包含所有状态,但不提供对其的公共访问。 GameStateAccess
将成为 GameStateData
的朋友并提供对其私人数据的访问。 GameStateAccess
的构造函数将采用指向 GameStateData
的引用或指针,并将锁定该数据的互斥锁。析构函数将释放互斥锁。这样一来,您用于操作状态的代码将被简单地编写为一个块,其中 GameStateAccess
对象在范围内。
但仍然存在一个漏洞:如果从此GameStateAccess
类返回的对象是指向可变对象的指针或引用,则此设置不会阻止您的代码携带此类指针超出受保护的范围互斥体。为了防止这种情况发生,要么注意你是如何写东西的,要么使用一些自定义的类似指针的模板类,一旦GameStateAccess
超出范围就可以清除它,或者确保你只通过值而不是引用传递东西。
示例
使用C++11,上述锁管理思路可以实现如下:
class GameStateData
private:
std::mutex _mtx;
int _val;
friend class GameStateAccess;
;
GameStateData global_state;
class GameStateAccess
private:
GameStateData& _data;
std::lock_guard<std::mutex> _lock;
public:
GameStateAccess(GameStateData& data)
: _data(data), _lock(data._mtx)
int getValue() const return _data._val;
void setValue(int val) _data._val = val;
;
void LogicManager::performStateUpdate
int valueIncrement = computeValueIncrement(); // No lock for this computation
GameStateAccess gs(global_state); // Lock will be held during this scope
int oldValue = gs.getValue();
int newValue = oldValue + valueIncrement;
gs.setValue(newValue); // still in the same transaction
// free lock on global state
cleanup(); // No lock held here either
循环终止指示器
3) 我让所有线程都访问“bool run”以检查是否继续它们的循环
while(gs->run) ....
如果我在 EventManager 中收到退出消息,run 会设置为 false。我是否需要同步该变量?我可以将其设置为 volatile 吗?
对于这个应用程序,一个易失但不同步的变量应该没问题。您必须将其声明为 volatile 以防止编译器生成缓存该值的代码,从而隐藏另一个线程的修改。
作为替代方案,您可能希望为此使用 std::atomic
变量。
指针间接开销
4) 不断取消引用指针等是否会对性能产生影响?例如
gs->objects->entitylist.at(2)->move();
所有这些->
和.
是否会导致严重的减速?
这取决于替代品。在许多情况下,编译器将能够保留例如的值。上面代码中的gs->objects->entitylist.at(2)
,如果重复使用,就不用一遍遍地计算了。一般来说,我认为由于所有这些指针间接导致的性能损失是次要问题,但这很难确定。
【讨论】:
谢谢。至于2),能否给我一个代码sn-p的例子? @Zeke,我添加了一个示例。【参考方案2】:这是处理事情的好方法吗? (class Gamestate
)
1) 这是处理事情的好方法吗?
是的。
意思是使用“全局”Gamestate 类是个好主意吗?
是的,如果 getter/setter 是线程安全的。
有没有更好的方法?
没有。数据对于游戏逻辑和表示都是必需的。如果你把它放在一个子例程中,你可以删除 global 游戏状态,但这只会将你的问题转移到另一个函数。全局 Gamestate 还可以让您非常轻松地保护当前状态。
互斥体和获取器/设置器
2) 我的意图是让 Gamestate 在 getter/setter [...] 中有互斥锁。解决这个问题的最优雅的方法是什么?
这称为读/写问题。为此,您不需要公共互斥锁。请记住,您可以有很多读者,但只有一位作者。您可以为读取器/写入器实现一个队列并阻止其他读取器,直到写入器完成。
while(gs->run)
我是否需要同步该变量?
当变量的非同步访问可能导致未知状态时,它应该被同步。所以如果run
在渲染引擎开始下一次迭代后立即设置为false
,并且Gamestate已经被销毁,那将会造成混乱。但是,如果gs->run
只是一个指示循环是否应该继续,它是安全的。
请记住,逻辑和渲染引擎应该同时停止。如果不能同时关闭两者,请先停止渲染引擎以防止冻结。
取消引用指针
4) 不断取消引用指针等是否会对性能产生影响?
有两个优化规则:
-
不要优化
尚未优化。
编译器可能会处理这个问题。作为程序员,您应该使用对您来说最易读的版本。
【讨论】:
能否给我一个 2) 的代码 sn-p,或者给我一个处理该问题的资源的链接?以上是关于构建和同步多线程游戏循环的主要内容,如果未能解决你的问题,请参考以下文章