组织和同步多线程游戏循环

6

我在我的游戏循环中遇到了一个轻微的线程安全难题。下面是三个线程(包括主线程)的代码,它们旨在协同工作。一个用于事件管理(主线程),一个用于逻辑,一个用于渲染。正如您在下面所看到的那样,这三个线程都存在于自己的类中。在基本测试中,该结构没有问题。此系统使用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在getters / setters中具有互斥锁,但是这对于读取不起作用,因为当对象仍然被锁定时无法返回对象,这意味着我必须在getters / setters之外放置同步并使互斥锁公开化。这也意味着我将有很多不同资源的互斥锁。解决此问题的最优雅方法是什么? 3)我让所有线程访问“bool run”来检查是否继续它们的循环。
while(gs->run){
....
}

如果在EventManager中收到退出消息,则将run设置为false。我需要同步该变量吗?我应该将其设置为volatile吗?

4)不断解引用指针是否会影响性能?例如,gs->objects->entitylist.at(2)->move(); 所有这些“->”和“.”会导致任何主要的减速吗?

2个回答

6

全局状态

1) 这是一个好的做法吗?也就是说,使用“全局”Gamestate类是一个好主意吗?有没有更好的方法?

对于游戏而言,相较于一些可重用的代码,我认为全局状态已经足够了。你甚至可以避免在程序中传递gamestate指针,将其变成全局变量。

同步

2) 我的意图是在getters/setters中为Gamestate添加互斥锁,但这对于读取操作来说并不起作用,因为我不能在对象仍处于锁定状态时返回它,这意味着我必须在getters/setters之外进行同步,并使互斥锁公开化。这也意味着我需要为所有不同的资源拥有大量的互斥锁。有什么更优雅的方法来解决这个问题?

我会尝试以事务的方式思考这个问题。将每个状态更改都包装在自己的互斥锁代码中不仅会影响性能,而且如果代码获取一个状态元素,在之后执行一些计算并设置值,而其他代码在其中修改了相同的元素,则可能导致实际上的错误行为。因此,我会尝试以这样的方式构建LogicManagerRenderer,使得所有与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) I have all of the threads accessing "bool run" to check if to continue their loops

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

run gets set to false if I receive a quit message in the EventManager. Do I need to synchronize that variable at all? Would I set it to volatile?

对于这个应用程序,一个易失但不同步的变量应该是可以的。你必须声明它易失以防止编译器生成缓存该值的代码,从而隐藏另一个线程的修改。
作为替代方案,您可能想要使用std::atomic变量。
指针间接引用开销
这取决于其他选择。在许多情况下,如果以上代码中的gs->objects->entitylist.at(2)被重复使用,编译器将能够保持其值,并且不必一遍又一遍地计算它。一般来说,我认为由于所有这些指针间接引用而导致的性能损失是次要的问题,但这很难确定。

谢谢。至于第二点,您能否给我一个代码片段示例? - Zeke
@Zeke,我已经添加了一个例子。 - MvG

5

这是一个好的做法吗?(class Gamestate)

1) 这种方法好吗?

是的。

意思是将“全局”Gamestate类用于此是否是个好主意?

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

有更好的方法吗?

没有。数据对于游戏逻辑和表示都是必要的。如果将其放在子例程中,则可以删除全局gamestate,但这只会将您的问题传输到另一个函数中。全局Gamestate还将使您轻松保存当前状态。

Mutex和getter/setter

2) 我的意图是在getter/setter中拥有Gamestate的互斥锁[...]。解决这个问题的最优雅的方式是什么?

这称为读者/写者问题。您不需要公共互斥锁。只需记住,您可以有很多读者,但只能有一个编写者。您可以为读者/编写者实现队列,并阻止其他读者,直到编写者完成。

while(gs->run)

我需要同步该变量吗?

每当变量的非同步访问可能导致未知状态时,都应该将其同步。因此,如果run将在渲染引擎启动下一个迭代并且Gamestate已被销毁之后立即设置为false,则会导致混乱。但是,如果gs->run仅是指示循环是否应继续,则是安全的。

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

解除指针引用

4) 不断解除指针引用等对性能有影响吗?

有两个优化规则:

  1. 不要优化
  2. 暂时不要优化。

编译器可能会处理此问题。作为程序员,您应该使用最适合您的可读性版本。


你能给我提供一个关于2)的代码片段,或者一个处理该问题的资源链接吗? - Zeke

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接