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

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 并公开互斥锁。这也意味着我将为所有不同的资源拥有大量的互斥锁。解决这个问题的最优雅的方法是什么?

我会尝试从交易的角度来考虑这一点。将每个状态更改包装到自己的互斥锁代码中不仅会影响性能,而且如果代码获取一个状态元素,对其执行一些计算并稍后设置值,而其他一些代码修改了之间的相同元素。所以我会尝试构建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 吗?

对于这个应用程序,一个易失但不同步的变量应该没问题。您必须将其声明为 volatile 以防止编译器生成缓存该值的代码,从而隐藏另一个线程的修改。

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

指针间接开销

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

这取决于替代品。在许多情况下,编译器将能够保留例如的值。上面代码中的gs-&gt;objects-&gt;entitylist.at(2),如果重复使用,就不用一遍遍地计算了。一般来说,我认为由于所有这些指针间接导致的性能损失是次要问题,但这很难确定。

【讨论】:

谢谢。至于2),能否给我一个代码sn-p的例子? @Zeke,我添加了一个示例。【参考方案2】:

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

1) 这是处理事情的好方法吗?

是的。

意思是使用“全局”Gamestate 类是个好主意吗?

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

有没有更好的方法?

没有。数据对于游戏逻辑和表示都是必需的。如果你把它放在一个子例程中,你可以删除 global 游戏状态,但这只会将你的问题转移到另一个函数。全局 Gamestate 还可以让您非常轻松地保护当前状态。

互斥体和获取器/设置器

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

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

while(gs-&gt;run)

我是否需要同步该变量?

当变量的非同步访问可能导致未知状态时,它应该被同步。所以如果run在渲染引擎开始下一次迭代后立即设置为false,并且Gamestate已经被销毁,那将会造成混乱。但是,如果gs-&gt;run 只是一个指示循环是否应该继续,它是安全的。

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

取消引用指针

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

有两个优化规则:

    不要优化 尚未优化。

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

【讨论】:

能否给我一个 2) 的代码 sn-p,或者给我一个处理该问题的资源的链接?

以上是关于构建和同步多线程游戏循环的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程与并发库高级应用-传统线程同步通信技术

如何在多线程中同步“for”循环计数器?

多线程游戏服务器的基本设计?

Java多线程与并发库4.传统线程同步通信技术

从零开始学多线程之构建快

多线程同步访问对象,循环打印1234