2011-02-15 22 views
3

我的应用程序由两个线程:如何存储和推送模拟状态,同时对每秒更新产生的影响最小?

  1. GUI线程(使用Qt)
  2. 模拟螺纹

我使用两个线程的原因是为了保证GUI作出响应,同时让辛线程尽可能快地旋转。

在我的GUI线程中,我以30-60的FPS渲染SIM中的实体;然而,我希望我的模拟器能够“前方紧缩” - 可以这么说 - 并且排队等待最终绘制的游戏状态(认为流式视频,你有一个缓冲区)。

现在对于我渲染的每个帧,我需要相应的模拟“状态”。所以,我的SIM线程看起来像:

while(1) { 
    simulation.update(); 
    SimState* s = new SimState; 
    simulation.getAgents(s->agents); // store agents 
    // store other things to SimState here.. 
    stateStore.enqueue(s); // stateStore is a QQueue<SimState*> 
    if(/* some threshold reached */) 
     // push stateStore 
} 

SimState样子:

struct SimState { 
    std::vector<Agent> agents; 
    //other stuff here 
}; 

与仿真:: getAgents样子:

void Simulation::getAgents(std::vector<Agent> &a) const 
{ 
    // mAgents is a std::vector<Agent> 
    std::vector<Agent> a_tmp(mAgents); 
    a.swap(a_tmp); 
} 

Agentš本身是比较复杂的类。成员是一堆int s和float s和两个std::vector<float> s。

使用这个当前设置,sim不能紧缩必须比GUI线程绘制得更快。我已经验证目前的瓶颈是simulation.getAgents(s->agents),因为即使我没有推出,每秒更新速度也很慢。如果我评论这一行,我会看到每秒更新次数有几个数量级的提高。

那么,我应该用什么种类的容器来存储模拟状态?我知道atm上有一堆拷贝,但其中一些是不可避免的。我应该将Agent*存储在向量中而不是Agent?在现实中,模拟不是循环的,而是使用Qt的QMetaObject::invokeMethod(this, "doSimUpdate", Qt::QueuedConnection);,所以我可以使用信号/插槽在线程之间进行通信;不过,我已使用while(1){}验证了更简单的版本,问题仍然存在。

+0

我很想知道你是如何解决这个问题的,尽管它可能已经有一段时间了。我遇到类似的问题 – woosah 2015-03-02 03:11:11

回答

5

尝试重新使用您的SimState对象(使用某种池机制),而不是每次都分配它们。经过几次模拟循环后,重新使用的SimState对象将具有增长到所需大小的向量,从而避免重新分配并节省时间。

一个实现池的简单方法是最初将一堆预先分配的SimState对象推送到std::stack<SimState*>上。请注意,堆栈比队列更受欢迎,因为您希望将更可能在缓存内存中“更热”的SimState对象(最近使用的SimState对象位于堆栈顶部)。您的模拟队列将SimState对象从堆栈弹出,并使用计算的SimState填充它们。然后将这些计算出的SimState对象推送到生产者/消费者队列中以供给GUI线程。在被GUI线程渲染后,它们被推回到SimState栈(即“池”)。尽量避免不必要的复制SimState对象。在“管道”的每个阶段直接使用SimState对象。

当然,你必须使用正确的同步机制在SIMSTATE栈和队列,以避免竞争条件。 Qt可能已经拥有线程安全的堆栈/队列。无锁栈/队列可能会加速竞争(英特尔线程构建模块提供这种无锁队列)。看到计算一个SimState花费大约1/50秒的时间,我怀疑争用会成为一个问题。

如果您SIMSTATE池耗尽,那么就意味着你的模拟线太“超前”,可以再等待一段SIMSTATE对象返回到池中。仿真线程应该阻塞(使用条件变量),直到SimState对象再次在池中可用。您SIMSTATE池的大小相当于多少如何SIMSTATE可缓冲(例如〜50个对象池为您提供了长达约1秒的紧缩提前时间)。

您也可以尝试运行并行仿真线程来充分利用多核处理器。 Thread Pool模式在这里很有用。但是,必须注意计算的SimStates按照正确的顺序排队。按时间戳排序的线程安全优先级队列可能适用于此。

这里的流水线架构,我建议的简单示意图:

pipeline architecture

(右键单击并选择查看图像更清晰的视野。)

(注:池和队列举行SimState由指针,不值!)

希望这会有所帮助。


如果您打算重新使用SIMSTATE对象,那么你的Simulation::getAgents方法是低效的。这是因为vector<Agent>& a参数很可能已经有足够的能力来保存代理列表。

你现在这样做的方式会扔掉这个已经分配的矢量,并从头开始创建一个新的矢量。

IMO,您getAgents应该是:

void Simulation::getAgents(std::vector<Agent> &a) const 
{ 
    a = mAgents; 
} 

是的,你输了异常安全的,但你可能会提高性能(特别是与可重复使用SIMSTATE方法)。


另一个想法:你可以尝试让你的代理对象固定大小,使用C数组(或boost::array)和“计数”变量,而不是std::vector为代理的浮动列表成员。只需使固定大小的阵列足够大以适应模拟中的任何情况。是的,你会浪费空间,但你可能会获得很多速度。

然后,您可以使用固定大小的对象分配器(如boost::pool)集中你的代理和通过指针(或shared_ptr)通过他们周围。这将消除大量堆分配和复制。

可以单独使用或组合使用上述思路用了这个想法。这个想法似乎比上面的管道更容易实现,所以你可能想先尝试一下。


另一个想法:与其运行模拟循环线程池,可以分解模拟成几个阶段,并在它自己的线程中执行的每个阶段。生产者/消费者队列用于在阶段之间交换SimState对象。为了有效,不同的阶段需要大致类似的CPU工作负载(否则,一个阶段将成为瓶颈)。这是利用并行性的另一种方式。

+0

哇!神奇的答案,我绝对没有考虑使用SimState对象池。你有什么要补充的关于`getAgents`方法吗? (1)传递一个引用,填充一个临时向量(3)交换这两个参数的过程,是实现它的最好方法吗? – Casey 2011-02-15 20:40:05

相关问题