如何减少多线程引擎中线程之间传递消息的麻烦?


18

我目前正在使用的C ++引擎分为几个大线程-生成(用于创建程序内容),游戏性(用于AI,脚本,模拟),物理和渲染。

线程之间通过小消息对象相互通信,这些消息对象在线程之间传递。在执行步进操作之前,一个线程会处理其所有传入消息-更新以进行转换,添加和删除对象等。有时,一个线程(Generation)会创建某种东西(Art)并将其传递给另一个线程(Rendering)以实现永久所有权。

在此过程的早期,我注意到了两点:

  1. 消息传递系统很麻烦。创建新的消息类型意味着要对基础Message类进行子类化,为其类型创建一个新的枚举,并编写有关线程应如何解释新消息类型的逻辑。这是开发的重灾区,并且容易出现拼写错误。(Sidenote-从事此工作使我很欣赏动态语言的出色表现!)

    有一个更好的方法吗?我是否应该使用boost :: bind之类的工具使之自动运行?我担心如果这样做,我会失去说话的能力,无法根据类型对消息进行排序。不确定是否需要这种管理。

  2. 第一点很重要,因为这些线程进行了大量通信。创建和传递消息是使事情发生的重要部分。我想简化该系统,但也欢迎其他可能同样有用的范例。我应该考虑使用其他不同的多线程设计来简化此过程吗?

    例如,有些资源很少被写入,但是经常从多个线程中读取。我是否应该对所有线程都可以访问的共享数据(受互斥锁保护)持开放态度?

这是我第一次从头开始考虑多线程设计。在这个早期阶段,我实际上认为进展非常顺利(正在考虑),但我担心扩展以及实现新内容的效率问题。


6
这里确实没有一个直接的定向问题,因此,此帖子不适合本网站的问答风格。我建议您将您的帖子分解为不同的帖子,每个问题一个,然后重新关注这些问题,以便他们询问您实际遇到的特定问题,而不是模糊的提示或建议。

2
如果您想进行更一般的对话,我建议您在gamedev.net的论坛上尝试这篇文章。如Josh所说,由于您的“问题”不是一个特定的问题,因此很难以StackExchange格式适应。
Cypher

谢谢你们的反馈!我有点希望拥有更多知识的人可能拥有单一的资源/经验/范例,可以同时解决我的多个问题。我感觉到一个大主意可以将我的各种问题综合成一件事,而我却一直在想,我在想一个经验比我可能认识到更多的人。 !
Raptormeat,2011年

我将您的标题重命名为更特定于消息传递,因为“提示”类型的问题暗示没有特定的问题要解决(因此,如今,我将其称为“不是一个真正的问题”)。
四分

您确定需要用于物理和游戏的单独线程吗?这两个似乎很交织。同样,很难在不知道每个人如何沟通以及与谁沟通的情况下就如何提供建议。
Nicol Bolas

Answers:


10

对于更广泛的问题,请考虑尝试找到尽可能减少线程间通信的方法。如果可以的话,最好完全避免同步问题。这可以通过以下方式来实现:对数据进行双重缓冲,引入单次更新延迟,但大大简化了使用共享数据的工作。

顺便说一句,您是否考虑过不按子系统进行线程化,而是使用线程生成或按任务分叉的线程池?(见在问候你的具体问题,使用线程池。)这篇短文概述池模式的用途和用法简洁。查看这些信息丰富的答案也。如此处所述,线程池可以提高可伸缩性。它是“一次编写,可在任何地方使用”,而不是每次编写新游戏或引擎时都必须使基于子系统的线程运行得很好。也有很多可靠的第三方线程池解决方案。如果需要减轻产生和销毁线程的开销,那么从线程生成开始,然后再处理线程池,会更简单。


1
对于要检出的特定线程池库有什么建议吗?
imre

尼克-非常感谢您的回复。关于您的第一点,我认为这是个好主意,也许是我前进的方向。目前还为时过早,我还不知道需要什么来加倍缓冲。随着时间的推移,我会牢记这一点。第二点-感谢您的建议!是的,线程任务的好处显而易见。我会阅读您的链接并考虑一下。不确定100%是否适合我/如何使其适合我,但我一定会认真考虑的。谢谢!
Raptormeat

1
@imre检验Boost库-它们具有期货,这是一种处理这些问题的好方法。
乔纳森·迪金森

5

您询问了不同的多线程设计。我的一个朋友告诉我这种方法,我认为这很酷。

这个想法是每个游戏实体都有2个副本(我知道这很浪费)。一个副本将是当前副本,另一个副本将是过去副本。当前副本严格只能写,而过去副本严格只读。当您进行更新时,可以将实体列表的范围分配给尽可能多的线程。每个线程都具有对在指定范围内的当前副本的写访问权限,并且每个线程都具有对实体的所有过去副本的访问权限,因此可以使用过去副本中的数据更新锁定的当前副本,而无需锁定。在每帧之间,当前副本变为过去副本,但是您要处理角色交换。


4

仅使用C#,我们就遇到了同样的问题。在认真思考过是否容易创建新消息之后,我们能做的最好的事情就是为它们创建一个代码生成器。这有点丑陋,但可用:仅给出消息内容的描述,它会生成消息类,枚举,占位符处理代码等-所有这些代码每次几乎都是相同的,而且确实容易出现错字。

我对此并不完全满意,但是总比手工编写所有代码要好。

关于共享数据,最佳答案当然是“取决于”。但是通常,如果经常读取某些数据并且许多线程需要这些数据,则共享它是值得的。为了线程安全,最好的选择是将其设为不可变的,但是如果这是不可能的,则互斥体可能会做到这一点。在C#中有一个ReaderWriterLockSlim专门为此类情况设计的类。我确定有C ++等效项。

线程通信的另一个想法(可能解决了您的第一个问题)是传递处理程序而不是消息。我不确定如何在C ++中解决这个问题,但是在C#中,您可以将一个delegate对象发送到另一个线程(例如,将其添加到某种消息队列中),然后从接收线程中实际调用此委托。这样就可以在现场创建“临时”消息。我只是玩弄这个主意,从来没有在生产中实际尝试过,所以实际上可能很糟糕。


谢谢你提供这些很棒的信息!关于处理程序的最后一点类似于我提到的关于使用绑定或函子来传递函数的内容。我有点喜欢这个主意-我可能会尝试一下,看看它是否很烂:D可能首先创建一个CallDelegateMessage类,然后将脚趾浸入水中。
Raptormeat

1

我只是处于一些线程游戏代码的设计阶段,因此我只能分享自己的想法,而不能分享任何实际经验。话虽如此,我正在考虑以下方面:

  • 大多数游戏数据应共享为只读访问权限
  • 使用一种消息传递可以写入数据。
  • 为了避免在另一个线程读取数据时更新数据,游戏循环具有两个不同的阶段:读取和更新。
  • 在读取阶段:
  • 所有共享数据对于所有线程都是只读的。
  • 线程可以计算内容(使用线程本地存储),并产生更新请求(基本上是命令/消息对象),该请求放置在队列中,稍后再应用。
  • 在更新阶段:
  • 所有共享数据都是只写的。假定数据处于未知/不稳定状态。
  • 这是处理更新请求对象的地方。

我认为(尽管我不确定)从理论上讲,这应该意味着在读取和更新阶段,任何数量的线程都可以在最小同步的情况下并发运行。在读取阶段,没有人正在写入共享数据,因此不会出现并发问题。更新阶段,这比较棘手。对一条数据进行并行更新将是一个问题,因此这里需要进行一些同步。但是,只要它们在不同的数据集上运行,我仍然可以运行任意数量的更新线程。

总的来说,我认为这种方法很适合线程池系统。有问题的部分是:

  • 同步更新线程(确保没有多个线程尝试更新同一数据集)。
  • 确保在读取阶段没有线程可以意外写入共享数据。恐怕编程错误的余地太大了,我不确定调试工具会轻易捕获其中的多少错误。
  • 编写代码的方式使您不能依赖中间结果可立即读取。也就是说,x += 2; if (x > 5) ...如果x是共享的,则无法写入。您需要制作x的本地副本,或者产生更新请求,并且仅在下一次运行中执行条件操作。后者将意味着大量额外的线程局部状态保留样板代码。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.