构建和同步多线程游戏循环

Structuring and Synchronizing a Multithreaded Game Loop

本文关键字:游戏 循环 多线程 同步 构建      更新时间:2023-10-16

我遇到了一个关于游戏循环线程安全的小难题。我下面有3个线程(包括main),它们将一起工作。一个用于事件管理(主线程),一个用于逻辑,另一个用于渲染。所有这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) 这是处理事情的好方法吗?这意味着拥有一个"全局"游戏状态类是一个好主意吗?有更好的方法吗?

2) 我的意图是让Gamestate在getters/ssetter中有互斥体,但这对读取不起作用,因为当对象仍然被锁定时,我无法返回它,这意味着我必须将同步放在getters/Sesetter之外,并将互斥体公开。这也意味着,对于所有不同的资源,我将拥有大量的互斥对象。解决这个问题最优雅的方法是什么?

3) 我让所有访问"bool run"的线程检查是否继续它们的循环

while(gs->run){
....
}

如果我在EventManager中收到退出消息,run将设置为false。我需要同步那个变量吗?我会把它设置为易失性吗?

4) 不断取消引用指针之类的东西会对性能产生影响吗?例如gs->objects->entitylist.at(2)->move();执行所有这些"->"answers"。"导致经济大幅放缓?

全局状态

1)这是一种很好的做事方式吗?这意味着拥有一个"全局"游戏状态类是一个好主意吗?有更好的方法吗?

对于一个游戏,与一些可重复使用的代码相比,我认为全局状态就足够了。您甚至可以避免传递游戏状态指针,而是将其作为全局变量。

同步

2)我的意图是让Gamestate在getters/ssetter中有互斥体,但这对读取不起作用,因为当对象仍然被锁定时,我无法返回它,这意味着我必须将同步放在getters/Sesetter之外,并将互斥体公开。这也意味着,对于所有不同的资源,我将拥有大量的互斥对象。解决这个问题最优雅的方法是什么?

我会试着从交易的角度来考虑这一点。将每一个状态更改封装到自己的互斥锁代码中不仅会影响性能,而且如果代码获得一个状态元素,对其执行一些计算并在以后设置值,而其他代码在其间修改了同一个元素,则可能会导致实际的不正确行为。因此,我尝试以这样的方式构建LogicManagerRenderer,即与Gamestate的所有交互都捆绑在几个地方。在该交互的持续时间内,线程应该持有该状态的互斥对象。

如果您想强制使用互斥,那么您可以创建一些至少有两个类的构造。让我们称它们为GameStateDataGameStateAccessGameStateData将包含所有状态,但不提供对其的公共访问。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,以防止编译器生成缓存该值的代码,从而隐藏另一个线程的修改。

作为替代方案,您可能需要为此使用std::atomic变量。

指针间接开销

4)不断取消引用指针等是否会对性能产生影响?例如gs->objects->entitylist.at(2)->move();所有这些->.都会导致任何重大减速吗?

这取决于备选方案。在许多情况下,如果重复使用,编译器将能够在上面的代码中保留例如gs->objects->entitylist.at(2)的值,并且不必反复计算。一般来说,我会认为所有这些指针间接导致的性能损失是次要的问题,但这很难确定。

这是处理事情的好方法吗?(class Gamestate)

1)这是一种很好的做事方式吗?

是。

这意味着使用"全局"游戏状态类是个好主意吗?

是的,如果getter/setter是线程安全的。

有更好的方法吗?

否。数据对于游戏逻辑和表示都是必要的。如果你把全局游戏状态放在子程序中,你可以删除它,但这只会把你的问题转移到另一个函数。全局游戏状态也将使您能够非常容易地保护当前状态。

Mutex和getter/setter

2)我的意图是让Gamestate在getters/ssetters中有互斥对象[…]。解决这个问题最优雅的方法是什么?

这被称为读写器问题。您不需要公共互斥。请记住,你可以有很多读者,但只有一个作家。您可以为读卡器/写入器实现一个队列,并在写入器完成之前阻止其他读卡器。

while(gs->run)

我是否需要同步该变量?

每当对变量的非同步访问可能导致未知状态时,都应该对其进行同步。因此,如果在渲染引擎开始下一次迭代后立即将run设置为false,并且游戏状态已被破坏,则会导致混乱。然而,如果gs->run只是循环是否应该继续的指示符,那么它是安全的。

请记住,逻辑和渲染引擎都应该同时停止。如果不能同时关闭两者,请首先停止渲染引擎以防止冻结。

取消引用指针

4)不断取消引用指针等是否会对性能产生影响?

有两条优化规则:

  1. 不优化
  2. 尚未优化

编译器可能会处理这个问题。作为一名程序员,你应该使用对你来说可读性最好的版本。