我遇到了一个关于游戏循环线程安全性的温和难题。我下面的内容是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();做所有那些' - >'和'。'导致任何重大放缓?
答案 0 :(得分:6)
1)这是处理事情的好方法吗?意义是使用“全局”Gamestate类是个好主意吗?有更好的方法吗?
对于游戏而言,与一些可重复使用的代码相反,我会说全球状态已经足够好了。您甚至可以避免传递游戏状态指针,而是真正使它成为全局变量。
2)我的目的是让Gamestate在getter / setter中有互斥,除了不能读取因为我仍然无法返回对象,这意味着我必须将同步放在外面getter / setter并将互斥体公之于众。这也意味着我将为所有不同的资源提供大量的互斥量。解决这个问题最优雅的方式是什么?
我试图在交易方面考虑到这一点。将每个状态更改包装到其自己的互斥锁定代码中不仅会影响性能,而且如果代码获取一个状态元素,对其执行一些计算并稍后设置该值,则可能导致实际上不正确的行为,而其他一些代码则修改了两者之间相同的元素。因此,我尝试以这样的方式构建LogicManager
和Renderer
,以便将与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)我让所有线程访问“bool run”以检查是否继续其循环
如果我在EventManager中收到退出消息,while(gs->run){ .... }
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还可以让您轻松保护当前状态。
2)我的目的是让Gamestate在getter / setter中有互斥量[...]。解决这个问题最优雅的方式是什么?
这称为读写器问题。您不需要公共互斥锁。请记住,你可以有很多读者,但只有一位作家。您可以为读者/编写者实现一个队列,并阻止其他读者,直到编写完成。
while(gs->run)
我是否需要同步该变量?
每当变量的非同步访问可能导致未知状态时,应该同步它。因此,如果在渲染引擎启动下一次迭代后立即将run
设置为false
并且Gamestate已被销毁,则会导致混乱。但是,如果gs->run
仅仅是循环是否应该继续的指示,那么它是安全的。
请记住,逻辑和渲染引擎应该同时停止。如果你不能同时关闭两者,先停止渲染引擎,以防止冻结。
4)是否经常取消引用指针等对性能产生影响?
有两个优化规则:
编译器可能会处理这个问题。作为程序员,您应该使用最适合您的版本。