没有锁的独立物理线程

时间:2011-07-03 10:10:53

标签: c++ multithreading

我有一个经典的物理线程与图形线程问题:

假设我正在运行一个用于物理更新的线程和一个用于渲染的线程。

在物理线程(伪代码)中:

while(true)
{
  foreach object in simulation

     SomeComplicatedPhysicsIntegration( &object->modelviewmatrix);
     //modelviewmatrix is a vector of 16 floats (ie. a 4x4 matrix)
}

并在图形线程中:

while(true)
{
  foreach object in simulation
    RenderObject(object->modelviewmatrix);
}

现在理论上这不需要锁,因为一个线程只写入矩阵而另一个只是读取,而我并不关心过时的数据。

更新矩阵的问题不是原子操作,有时图形线程只会读取部分更新的矩阵(即并非所有16个浮点数都被复制,只有部分浮点数),这意味着矩阵的一部分来自一个物理框架和零件来自前一帧,这反过来意味着矩阵不再是仿射(即它基本上已损坏)。

有没有好的方法可以在不使用锁的情况下防止这种情况?我读到了使用双缓冲的可能实现,但我无法想象一种在不同步线程的情况下可以工作的方法。

编辑:我想我真正想要使用的是某些三重缓冲,就像它们在图形显示器上使用一样......有人知道三重缓冲算法的良好表现吗?

编辑2:确实使用非同步三重缓冲不是一个好的想法(如下面的答案所示)。物理线程可以运行多个循环,占用大量CPU并停止图形线程,计算最终从未渲染过的帧。

我选择了一个带有单个锁的简单双缓冲算法,其中物理线程在交换缓冲区之前仅在图形线程之前计算多达1帧。像这样:

物理:

while(true)
{
  foreach physicstimestep
   foreach object in simulation    
      SomeComplicatedPhysicsIntegration( &object->modelviewmatrix.WriteBuffer);
  LockSemaphore()
  SwapBuffers()
  UnlockSemaphore()
}

图形:

 while(true)
    {
     LockSemaphore()
      foreach object in simulation
        RenderObject(object->modelviewmatrix.ReadBuffer);
     UnlockSemaphore()
    }

听起来怎么样?

7 个答案:

答案 0 :(得分:5)

您可以在两个线程之间维护一个共享队列,并实现物理线程,这样它只有在完全填充该矩阵中的所有值后才会向队列添加一个矩阵。这假定物理线程在每次迭代时分配一个新矩阵(或者更具体地说,一旦将矩阵放入队列中,矩阵就被视为只读)。

因此,只要您的图形线程将矩阵拉出队列,就可以保证完全填充矩阵,并在生成矩阵时有效地表示模拟状态。

请注意,图形线程需要能够处理一个或多个迭代中队列为空的情况,并且为每个队列条目进行世界时间戳可能是个好主意,这样您就有了一个机制保持两个线程合理地同步而不使用任何正式的同步技术(例如,通过不允许图形线程使用任何具有未来时间戳的矩阵,并允许它在队列中向前跳过,如果下一个矩阵在过去是太过分了)。另请注意,您使用的任何队列都必须实现,以便在物理线程尝试在图形线程删除某些内容的同时添加某些内容时不会爆炸。

答案 1 :(得分:3)

  

但我无法想象一种在没有同步线程的情况下可行的方法。

无论您使用何种方案,同步线程都是绝对必要的。如果没有同步,您将面临物理线程将远远超出图形线程的风险,反之亦然。您的程序通常是一个提高时间的主线程,需要控制线程操作,而不是线程机制。

双缓冲是一种让您的物理和图形线程并行运行的方案(例如,您有一台多CPU或多核机器)。物理线程在一个缓冲区上运行,而图形线程在另一个缓冲区上运行。请注意,这会导致图形滞后,这可能是也可能不是问题。

答案 2 :(得分:2)

双缓冲背后的基本要点是复制要在屏幕上呈现的数据。

如果你运行某种锁定,那么你的模拟线程将始终在显示线程之前的正好一帧。模拟的每个数据都会被渲染。 (同步不一定非常重:一个简单的条件变量可以经常更新,并且非常便宜地唤醒渲染线程。)

如果你没有同步运行,你的模拟线程可能会模拟永远不会渲染的事件,如果渲染线程无法跟上的话。如果在数据中包含单调递增的世代数(在每个完整的模拟周期后将其更新),那么您的渲染线程可以简单地busy-wait生成两代数字(每个数据缓冲区一个)。

一旦一代(或两者)生成数大于最近渲染的代,将最新缓冲区复制到渲染线程中,更新最近渲染的计数器,并且开始渲染。完成后,返回忙碌的等待。

如果你的渲染线程太快,你可能会在繁忙的等待中咀嚼很多处理器。因此,只有在您希望定期跳过呈现某些数据并且几乎不需要等待更多模拟时,这才有意义。

答案 3 :(得分:1)

不要更新物理线程中的矩阵吗?

取一个块,(可能是你刚刚渲染的一行),并将其位置/大小/任何内容排入物理线程。将modelviewmatrix的行反转/转置/ whateverCleverMatrixStuff转换为另一个新行。将其发回给渲染线程。在渲染中的某个合适时间复制新行。也许你不需要复制它 - 也许你可以换掉一个'旧'矢量用于新的矢量并释放旧矢量?

这是可能的,还是你的矩阵的结构/操纵/对于这个太复杂了?

所有类型都取决于数据的结构,因此这种解决方案可能不合适/不可能。

RGDS, 马丁

答案 4 :(得分:1)

  

现在理论上这不需要锁,因为一个线程只写入矩阵而另一个只是读取,而我并不关心过时的数据。

注意:如果没有正确的同步,则无法保证读取线程会通过写入线程观察到任何更改。这方面被称为 visibility ,遗憾的是,它经常被忽视。

答案 5 :(得分:1)

假设无锁或近无锁更新实际上最能解决您的问题,听起来您希望物理线程计算新矩阵,然后立即即时更新所有这些值,所以无关紧要图形线程得到的矩阵的版本,只要(a)它最终得到它们,(b)它永远不会得到原来的一半和一半。

在这种情况下,听起来你想要一个物理线程:

/* pseudocode */
while (true) foreach (object in simulation) {
    auto new_object = object;
    SomeComplicatedPhysicsIntegrationInPlace(new_object)
    atomic_swap(object, new_object); // In pseudocode, ignore return value since nowhere
                                     // else changes value of object. In code, use assert, etc
}

替代方案,您可以计算整个模拟的新状态,然后交换这些值。实现这一目标的简单方法是:

/ * Psudocode * /    while(true){         simulation [1-global_active_idx] = simulation [global_active_idx];         foreach(模拟中的对象[global_inactive_idx]){             SomeComplicatedPhysicsIntegrationInPlace(对象);         }         global_active_idx = 1-global_active_idx; //隐含地假设这是原子的     }

图形线程应该不断渲染模拟[global_active_idx]。

事实上,这不起作用。它可以在许多情况下工作,因为通常情况下,将1写入包含0的内存位置实际上是大多数处理器上的原子位置,但它并不能保证工作。具体来说,其他线程可能永远不会重读该值。许多人通过声明变量volatile来躲避这一点,同时适用于许多编译器,但不能保证工作。

但是,要使任一示例工作,您需要的只是一个原子写指令,而C ++直到C ++ 0x才提供,但编译器很容易实现,因为大多数“写一个int”指令是原子,编译器只需要确保。

因此,您可以在物理循环结束时使用atomic_swap函数编写代码,并根据(a)锁定,写入,解锁序列实现该代码 - 这不应该显着阻塞图形线程,因为它只能阻塞一次内存写入的时间长度,并且可能每个帧只执行一次或(b)编译器内置的原子支持,例如。 http://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins.html

有类似的解决方案,例如。物理线程更新信号量,图形线程简单地将其视为值为0或1的变量;例如。物理线程将完成的计算发布到队列(内部以与上面类似的方式实现),并且图形线程不断呈现队列的顶部,如果队列下溢,则重复最后一个队列。

但是,我不确定我是否理解你的问题。如果物理没有改变,为什么有更新图形的点?为什么有任何一点比物理更快地更新物理,不能在每个块中进一步推断?锁定更新实际上有什么不同吗?

答案 6 :(得分:0)

您可以将两个矩阵放在一个缓存行大小(128个字节)中,并将其与128个字节对齐。这样,矩阵被加载到高速缓存行中,因此在一个块中写入和从存储器写入。这不是为了佯装,而是需要更多的工作。 (这只能解决在更新矩阵并获得非正交结果时读取矩阵的问题。)