6

关于我的游戏循环的线程安全,我遇到了一个温和的难题。我下面有 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 之外/setters 并公开互斥锁。这也意味着我将为所有不同的资源拥有大量的互斥锁。解决这个问题的最优雅的方法是什么?

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

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

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

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

4

2 回答 2

6

全局状态

1)这是处理事情的好方法吗?意思是使用“全局” Gamestate 类是个好主意吗?有没有更好的方法呢?

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

同步

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

我会尝试从交易的角度来考虑这一点。将每个状态更改包装到自己的互斥锁代码中不仅会影响性能,而且如果代码获取一个状态元素,对其执行一些计算并稍后设置值,而其他一些代码修改了之间的相同元素。所以我会尝试构建LogicManagerRenderer以这样的方式与游戏状态的所有交互都捆绑在几个地方。在该交互的持续时间内,线程应该在状态上持有一个互斥锁。

如果您想强制使用互斥锁,那么您可以创建一些至少有两个类的构造。让我们称它们为GameStateDataand GameStateAccessGameStateData将包含所有状态,但不提供对它的公共访问。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)上述代码中 eg 的值,而不必一遍又一遍地计算它。一般来说,我认为由于所有这些指针间接导致的性能损失是次要问题,但这很难确定。

于 2012-10-02T06:07:54.733 回答
5

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

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

是的。

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

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

有没有更好的方法呢?

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

互斥锁和获取器/设置器

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

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

while(gs->run)

我是否需要同步该变量?

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

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

取消引用指针

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

有两个优化规则:

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

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

于 2012-10-02T05:55:33.623 回答