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

时间:2012-10-02 05:09:02

标签: c++ multithreading thread-safety

我遇到了一个关于游戏循环线程安全性的温和难题。我下面的内容是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 gets设置为false。我是否需要同步该变量?我会将它设置为不稳定吗?

4)是否经常取消引用指针等对性能产生影响?例如gs-> objects-> entitylist.at(2) - > move();做所有那些' - >'和'。'导致任何重大放缓?

2 个答案:

答案 0 :(得分:6)

全球州

  

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 gets设置为false。我是否需要同步该变量?我会将它设置为不稳定吗?

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

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

指针间接开销

  

4)是否经常取消引用指针等对性能产生影响?例如gs->objects->entitylist.at(2)->move();所有->.会导致任何重大放缓吗?

这取决于替代方案。在许多情况下,编译器将能够保持例如上述代码中的gs->objects->entitylist.at(2),如果重复使用,则无需反复计算。一般来说,我会认为由于所有这个指针间接引起的性能损失是次要问题,但这很难说清楚。

答案 1 :(得分:5)

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

  

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

  

意味着使用“全局”Gamestate类是一个好主意吗?

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

  

有更好的方法吗?

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

Mutex和getter / setters

  

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

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

while(gs->run)

  

我是否需要同步该变量?

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

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

取消引用指针

  

4)是否经常取消引用指针等对性能产生影响?

有两个优化规则:

  1. 不优化
  2. 不要优化。
  3. 编译器可能会处理这个问题。作为程序员,您应该使用最适合您的版本。