多线程在游戏中共享资源,需要一些想法的反馈

MultiThread in Games sharing resources, Need some feedback on a idea

本文关键字:游戏 共享资源 多线程      更新时间:2023-10-16

我最近开始了一个制作游戏的项目(我有点新),我开始思考如何实现多线程来提高性能。

假设你在一个游戏中有一个单位,它有一个位置xy。这个位置是由一个线程从互联网上更新的,另一个线程使用x,y来渲染单元的图形(它必须知道它在哪里)。

现在假设您在这些变量上放置了一个互斥对象或sephamore(有点不确定哪一个是最好的)。当然,问题出在渲染线程上。你不能停下来等待,比赛会变得滞后。不过,这对互联网线程来说不是问题。除非出了什么问题,否则再过几毫秒对更新游戏无关紧要。

所以我在想如何解决这个问题,我有了一个主意。假设您创建了两组x,y(从现在起,我们只使用x来简化它,但您已经明白了)。所以你有x1x2

  • 现在互联网线程只更新了x1
  • 图形线程使用x1,然后将x2更新为其当前值

现在这个主意来了。如果x1处于来自互联网线程的锁定中,图形线程将只显示

"嘿,我等不及了。我会继续使用x2,因为这是一个很好的位置近似值。"

并且它将这样做,直到x1再次空闲。它会看起来像这样。

//Thread Graphics:
if (x1 is not locked){
    lock x1:
        use x1
    unlock x1:
    x2=x1
}else{
    use x2
}
//Thread Internet:
wait until x1 is unlocked:
lock x1:
    save data to x1
unlock x1:

现在我意识到这会占用一些额外的内存,但我认为这是值得的,至少如果你把这项技术的使用限制在关键的数据片段。

所以我的问题是:你们觉得这个主意怎么样?也许这已经是一种常见的技术了,只是我不知道它的名字。如果你对如何解决这样的问题有任何其他反馈,我将不胜感激。我认为这是大多数程序员的常见问题。

我喜欢这个想法,但我担心预测游戏中动作的后果。在我看来,你最好集中精力确保你的互斥对象保护(共享)数据在尽可能小的部分中得到保护。例如,不要在绘制过程中锁定整个对象,而是先锁定,复制所需内容,然后解锁,然后绘制。互联网方面也是如此。。。等待更新,获取更新,锁定,更新,解锁。

在我的游戏中,我在每一帧的末尾运行一个函数列表。

线程1:调用服务器以获取位置

线程2:在X,Y 上渲染对象的帧

线程2:检查来自线程1的工作。

线程2:没有工作,继续。

线程1:接收X,Y!

线程1:创建将设置最终X,Y 的工作对象

线程1:锁定线程2的工作队列。

线程1:将工作对象推送到工作队列

线程1:解锁线程2工作队列。

线程2:在X,Y 上渲染对象的帧

线程2:检查来自线程1的工作。

线程2:找到工作!正在锁定工作队列。。。

线程2:使用线程1工作设置对象X、Y。

线程2:删除工作对象

线程2:解锁工作队列

线程2:继续渲染循环

或者,也可以等待帧渲染完成,暂停片刻,更新值,然后取消暂停渲染线程。

使用额外内存不是个坏主意。

如果在处理数据时不锁定x1,则可以改进算法。

而不是这个

//Thread Graphics:
if (x1 is not locked){
    lock x1:
        use x1
    unlock x1:
    x2=x1
}else{
    use x2
}

使用类似的东西

//Thread Graphics:
if (x1 is not locked)
    lock x1:
        x2=x1
    unlock x1:
use x2

实际上,复制数据几乎正是我在某个时候(在遥远的未来)想要尝试的。这个想法在C++中也很简单。

如果实现值的基本类型(DualInt、DualFloat等)并提供所有标准运算符,则可以在内部将数据存储在两元素数组中。getter获取元素0,setter修改元素1。

诀窍在于转换它们。您所需要做的就是拥有一个读写器锁,其中getter和setter使用读写器部分,switcher使用写器部分。您可以让多个读者阅读(实际上是变老并设置新值),直到您想要切换。然后,您获取锁的写入器部分(它阻止新的读卡器并等待所有读卡器完成),并切换一个全局变量,该变量指示从哪个元素读取,向哪个元素写入。

不用大惊小怪,不用麻烦,也不用到处复制很多价值观。

这是一种维护线程安全的好方法,如果考虑双缓冲,可以进行扩展。在我参与的项目中,我们有一个线程安全的数据库,它遵循类似的原则:

  • 数据有两个副本,写缓冲区和读缓冲区
  • 每当数据库被写入时,它都会使用写缓冲区
  • 当从中读取时,它使用读取缓冲区
  • 在每帧结束时,缓冲区被交换,因此读缓冲区现在是写缓冲区,反之亦然

您应该能够在渲染线程中看到与双重缓冲的相似之处。这使我们能够在整个项目中保持线程安全。当然,代价是数据库中任何内容的内存使用量都会翻倍,因此它不适合存储大量数据。我们最大的成功通常是渲染数据和AI数据,它们只在单个线程中使用,所以这对我们来说不是什么大问题

编辑:我忘了提到第二个权衡:当我们更改数据库中的值时,它要到下一帧才会生效。这对我们来说并不是一个大问题,我们只需要确保在编写系统时牢记这一点。