我知道这些考虑因素的数百种变体已在网上发布。但是,我没有发现任何可以解决我确切问题的事情,所以我希望你能帮助我看清楚。
我目前正在使用OpenGL在Java中进行2D游戏开发。使用的语言和图形库与我的问题无关,因为它具有更一般的特征。
我正在尝试设计一个通用游戏循环,可以或多或少地用于任何具有中等重度图形(主要是位图纹理)的游戏,甚至可能更重的游戏逻辑(AI,碰撞检测等)
基本上我期望维护一个可以更新的对象的列表/数组/缓冲区(位置,速度和其他游戏相关的更新)和渲染(更新位置的纹理/帧)不会像光滑和有效一样停止可能的。
1)一个用于更新+渲染的线程
我已经尝试并且只使用一个线程丢弃了一个顺序解决方案(当计算用户输入时,两个线程就完成了。)
显然,当交换缓冲区在硬件上阻塞时会浪费很多好的计算时间,这需要更高效的解决方案
2)一个用于更新的线程,一个用于渲染的线程
通过将程序拆分为更新线程和渲染线程并同步对共享缓冲区的访问,我应该能够确保相当稳定的帧速率。同步对共享缓冲区的访问可以通过多种方式完成,但它们都有一个共同点。它们都禁止线程并发。虽然这可能是一个公平的权衡,但我想知道是什么使得同步成为必要。
3)与2相同,但没有同步
我确实理解了不小心实现并发线程可能导致的许多问题。生产者/消费者,读者/作者和类似的情况导致潜在的僵局。但是,如果满足以下条件(并且它们应该是),我不明白为什么我需要确保共享数据的同步:
-
那么......我在这里错过了哪些明显的东西?为什么我需要同步进行此设置?
欢迎提出任何想法,意见或建议。或者,如果这个特定问题已在其他地方得到解决,我将不胜感激。
答案 0 :(得分:2)
我决定花一点时间测试这个,因为我从这个网站得到了很多好的答案,我想我会发布这个来完成这个问题。也许别人会发现这些信息很有用。
我做了一个简单的精灵渲染应用程序的3个不同的实现,其中更新和渲染在不同的线程中运行。
1)没有同步
渲染器最高运行速度为60 FPS。 Updater运行速度尽可能快 要更新和呈现的精灵存在于两个线程共享的列表中。没有同步,因此线程只是随意读取和写入数据。
2)共享数据的同步
渲染器最高运行速度为60 FPS。 Updater以与渲染器相同的速度运行 要更新和呈现的数据存在于两个线程共享的列表中。该列表完全同步。 Updater更新列表中的所有精灵。然后渲染器获得对列表的访问权限,并将所有精灵渲染到屏幕上。
3)同步使用了双重渲染队列
渲染器最高运行速度为60 FPS。 Updater以与渲染器相同的速度运行 Updater更新列表并将sprite发送到2渲染队列的被动队列。同时,渲染器在活动渲染队列中渲染精灵。当Updater将最后一个对象复制到被动渲染队列时,它会尝试交换主动和被动队列。如果渲染器未完成呈现前一个队列,则交换将阻止。这是唯一的阻塞同步。一旦渲染器完成当前帧,就进行交换,渲染器可以开始渲染新队列,并且Updater可以开始更新并发送到另一个(现在是被动的)队列。
我在每个方法上运行了3次测试,在这里我计算了每秒执行更新和渲染的次数。
测试1:
精灵的数量足够低,因此渲染器可以全速运行(60 FPS)
每个精灵的更新逻辑太重,不允许更新程序保持同步。
测试2:
精灵的数量太高,以至于渲染器无法全速运行
每个精灵的更新逻辑非常简单,因此它们可以跟上。
测试3:
精灵的数量恰好足以使渲染器保持低于最大速度
每个精灵的更新逻辑都非常重,足以让Updater保持低于渲染器的最大速度。
无同步 - 测试1:
渲染器每秒运行60次(最大速度)
Updater每秒运行45次。
无同步 - 测试2:
渲染器每秒运行24次
Updater每秒运行1150次。
无同步 - 测试3:
渲染器每秒运行58次
Updater每秒运行51次。
同步共享数据 - 测试1:
渲染器每秒运行23次(最大速度)
Updater每秒运行24次。
同步共享数据 - 测试2:
渲染器每秒运行23次
Updater每秒运行23次。
同步共享数据 - 测试3:
渲染器每秒运行17次
Updater每秒运行17次。
同步双重排队 - 测试1:
渲染器每秒运行43次(最大速度)
Updater每秒运行43次。
同步双重排队 - 测试2:
渲染器每秒运行24次
Updater每秒运行24次。
同步双重排队 - 测试3:
渲染器每秒运行54次
Updater每秒运行54次。
正如你所指出的那样,Jirka,即使没有同步的方法似乎无害,只有一个作家可能会产生不必要的副作用,并且它肯定不会保持渲染帧的一致性。
使用双队列渲染比使用一个大型共享精灵列表渲染更快,这并不奇怪。然而,令人惊讶的是,如果你考虑到这样一个事实:在没有更新的情况下渲染多个帧没有任何好处,也没有渲染多次更新,那么双队列方法的最终结果实际上与非同步方法一样快。
可能有其他事情可以说或尝试过,但我已经看够了。我永远不会考虑再次使用Update / Render系统的非同步访问..
答案 1 :(得分:1)
可以在没有(很多)同步的情况下拥有一个独立的渲染和更新线程。看看
http://blog.slapware.eu/game-engine/programming/multithreaded-renderloop-part1/
和
http://blog.slapware.eu/game-engine/programming/multithreaded-renderloop-part2/
用于解释和实现(源+二进制文件)。这并不容易,但它正是你想要的。
答案 2 :(得分:0)
不同步的方法对你来说很合适;如果这是股票英特尔硬件 1 ,那么加倍。我还是不会用它。
非同步并发几乎永远不可靠的原因是处理器可以自由地在主RAM和缓存之间进行存储和加载。这可以破坏几乎任何不同步的协议。但是,正如你所说,没有人会注意到你的应用中场景是否从未突然改变;所有数据都会转到RAM中,迟早会被其他线程看到。
然而,您无法保证何时会以及在哪个序列中,这使您理论上可能会以奇怪的方式混合两个后续帧(在场景或其光照突然改变之前和之后)。
根据您的编程语言及其内存模型(我认为C ++早于C ++ 11?),您可能会发现轻量级同步原语,其保证的副作用是适当的内存屏障,其对性能的影响可以忽略不计。这是我建议的起点。极端性能优化(超出可证明安全性)应该是优化引擎的最后阶段。
1 )i86从不重新排序商店。我不认为这在任何地方都有记录,我不想依赖它。您仍然可以重新排序读取,因此无论如何在您的方案中都无济于事。