multithreading在更新循环中

好的,我正在研发游戏引擎,这是我以前的游戏循环:

Game::Run() { While(!m_GameDone) Client.Update(); LocalServer.Update(); } 

但是我想要做的是:

 Game:Run() { ClientThread = new CThread(...); ServerThread = new CThread(...); ClientThread.StartTask(Client::Run); ServerThread.StartTask(LocalServer::Run); CThreadSystem.WaitForTasksToFinish(); return; } 

然后Client :: Run和Server :: Run将是:

 Client::Run() { while(!m_Gamedone) { this.Update(); this.Render(); } } Server::Run() { while(!m_Gamedone) this.Update } 

所以我的两个大问题是…这是一个好主意吗? 以及如何让一个线程执行一个类实例的function? 我现在使用的线程系统是WinAPI和XTL(不是STL)。

更新:

好的,每个人都想告诉我锁和数据同步。 客户端和服务器线程不会共享任何常见的数据/对象。 客户端和服务器线程有自己的序列化更新循环,不使用任何常见的类或结构。 我只是想知道如何用类实例范围中的函数启动一个线程。

是的,这是一个好主意,尽管我会考虑将服务器端拆分成专用服务器。

其次,这里是如何启动一个Win32线程,并让它在一个类中调用一个函数。

 class MyClass { public: static void ThreadStartLoc(void* MyClassPtr) { ((MyClass*)MyClassPtr)->InternalThreadRun(); } private: void InternalThreadRun() { // Do your loop here } } 

用这个调用启动线程。

 MyClass* MyClassPtr = new MyClass(); _beginthreadex(NULL, StackSize, MyClass::ThreadStartLoc, MyClassPtr am, 0, NULL); 

假设你的Update()方法如下所示:

 for each entity in allEntities { entity.Update() } 

你需要同步(也就是locking)整个循环:

 lock(allEntities) { for each entity in allEntities { entity.Update() } } 

防止几乎任何multithreading的收益。 另一种方法是在每个实体更新调用周围进行同步:

 for each entity in allEntities { lock(entity) { entity.Update() } } 

同样,防止几乎任何multithreading增益,并且可能比仅同步整个循环慢得多。 同步是昂贵的:

避免在系统中拥有多个锁的另一个原因是进入和离开锁的成本。 最轻的锁使用一个特殊的比较/交换指令来检查锁是否被采取,如果不是,他们在一个单一的primefaces动作进入锁。 不幸的是,这个特殊的指令比较昂贵(通常比普通指令要长十到几百倍)

http://msdn.microsoft.com/en-ca/magazine/cc163744.aspx

也就是说,有multithreading解决scheme可以避免或最小化locking。 一种方法是使用1线程的核心游戏和其他线程的非核心游戏元素,如飞行模拟X [ 参考 ]生成和animation树木和动物。

另外一个方法就是每个游戏实体拥有2个副本(浪费,我知道)。 一份是present copy ,另past copypast copy 。 目前的副本是严格只写,而过去的副本是严格只读。 当你去更新时,你可以将实体列表的范围分配给你认为合适的线程。 每个线程都具有对指定范围内的当前副本的写入访问权限,并且每个线程都具有对所有实体的所有副本的读取访问权限,因此可以使用来自过去副本的数据更新指定的当前副本而不locking。 在每一帧之间,当前的副本成为过去的副本,但是你想要处理角色的交换。 [引用需要]

如果你没有注意到性能问题,我不会改变任何东西。 multithreading问题可能很难追查到。

使用与通过1方向问题的消息传递同步的重复数据。 它可以让你取消任何types的locking。 它也应该是一个非常灵活的系统。

这里有一些指针,它基本上是我制定的一个系统,用于绑定线程,多人networking复制和保存/加载。 这只是理论上的,我从来没有实现过,所以我不能说太多的performance。

这是很多文字,但input它也帮助我充实☺

首先明白,互斥锁将杀死性能,应该避免。 如果你有2个线程,他们都locking相同的游戏世界状态为他们的整个操作,然后他们会更慢,然后就是没有螺纹。 primefaces的东西是更好的性能明智的,但如果我们有创意,我们可以避免它。

不要围绕“对象”或“实体”进行同步。 将其基于各个属性。

给每个属性一个唯一的ID。 根据你的要求,一个简单的游戏可能只是使用一个无符号的int,像MMORPG这样更复杂的游戏可能会使用一个UUID (32bytes),其中的一部分是一个时间戳,应该允许在多个单独的服务器上生成id甚至相互了解一个统计上接近不可能的id碰撞的机会。 如果让这些ID保持同步,而不是dynamic生成它们,这也将有助于networking复制(最简单的方法是传输整个dynamic属性列表,它们的当前值和ID)。

命名的stringID可以帮助您编写脚本,因此您可以执行诸如“dungeon12.dragon3.health = 1000”之类的操作,并且/或者打印出一个可读的对象的所有ID及其值的列表(可以帮助编写关卡编辑器),但是名字会增加语法分析的开销,所以如果你确实需要这样的话,最好也可以使用线程和networking等常规编号的ID。

弄清楚什么属性是“静态的”(或者说const),什么是“dynamic的”。 关于同步,静态的东西可以被忽略。 内存也可以在线程之间共享,不需要任何同步,也不需要通过networking传输给客户端(只要它与你的游戏“交付”(自定义级别可能需要传输它,客户端没有它不过,也许你想制作一个非常灵活的引擎,让你可以改变网格上的顶点信息,这对于networking3D编辑器来说是非常有用的),例如不变的对象在关卡中,关卡本身,网格信息,dynamic数据可能只是你游戏数据的一小部分。

一个好主意是为每个属性赋予一个标志/时间戳,指示它在运行时被更改(或创建)(自从文件从磁盘加载后更改属性的一个标志,另一个属性自上次networking/线程复制传输)。 这使您可以快速而紧凑地保存,因为您只需要存储初始状态的更改。 您还可以让玩家跳到networking游戏的中间位置,并上传所有属性的列表。 它保存手动分离dynamic和静态的属性。

作为一个方面说明,如果你想几乎即时加载/保存find一种方法来保持所有的dynamic值与静态值分开,那么当你保存时,你可以“快照”当前的游戏状态,将其复制到另一个内存块当玩家继续玩时,产生后台线程将其写入磁盘。 加载可以刷新当前的dynamic状态并恢复保存的属性,而不影响保持不变的属性。

你的属性不再只是一个类中的基本数据types,而是属于自己的小类。

看看序列化。 这是从程序类获取数据并将其转化为可以传输或存储到磁盘的东西的技术。 东西可以序列化,以打扰二进制,明文和XML。 在它们之间进行select可能会很方便(networking和线程通信需要二进制文件,XML可能可用于从磁盘加载级别并为您提供可编辑的格式)。

为了提高性能,尽量将所有的属性信息保存在一个扁平的列表中,这样你就不必在树,八叉树,BSP或其他任何地方上下走动,并查询每个对象的所有信息。 它也有助于上面的保存/加载系统,因为你只是复制列表。

给每个线程自己的dynamic数据副本,我们将手动同步它们。 确保“客户端”线程只有dynamic信息需要知道,这是玩家可以实际看到的东西或在范围内的东西。 除了减less我们必须处理的内容之外,这也有助于防止networking作弊(如果没有背后的实体信息,则玩家不能进行黑客攻击,当然这需要您的服务器线程运行每个玩家实际上可以看到,如果你为每个玩家渲染一个真正的低分辨率的低graphics版本,但是一个简单的基于范围/裁剪的平截头体系统是更可能的,但是这仍然阻止玩家知道在地图的另一边的东西)。 你也许可以把它和你的3D绘图代码绑定在一起,因为在绘制可见和需要绘制的东西时,它会做类似的事情,将渲染场景graphics与游戏世界信息分开也很方便,没有任何意义在你的游戏对象中保持网格/纹理/ VBOs等等,这些都是渲染信息。 它还有助于保持您的游戏API不可知论的时候,从DirectX到OpenGL或某种ASCII渲染器的端口。

制定财产所有权/安全性。 几乎所有的东西都将由服务器“拥有”,除了一些特殊的类别,客户也可以发送他们的信息。 阻止客户拥有编辑他们不应该的价值的权力,但可以让您使用1个全局系统来管理属性(即不要将客户端信息视为与游戏世界的属性不同)。

计算信息的“stream量”。 例如,“服务器”线程将更新与游戏世界中的实体相关的“客户”线程。 它通常不会发生逆转,客户不应该直接更新实体信息,即使他们自己的角色,否则将允许黑客传送到地图周围。 相反,他们应该使用一个“他们拥有”的特殊控制器对象,服务器监视并用它来同步游戏世界中玩家的表示。

为了使每个线程/networking客户端之间保持同步,使用单向FIFO消息传递管道RPC问题。 基本上只有一个简单的FIFO,只有一个生产者和一个消费者可以实现,不需要互斥锁或primefaces信息。 从技术上讲,你可能需要一个单一的primefaces在自己的取决于你的编程语言/编译器做什么,它可能是所有简单的variables变化是primefaces,而不必做任何事情。 还取决于你需要在理论上是安全的,还是在现实世界中真正起作用。 C ++在大多数地方可能没有问题,但是从技术上讲,标准没有提到任何问题,所以在理论上你的计算机可能会爆炸,C ++ 11引入了primefaces<>。 我不知道C#是什么。 这些FIFO问题可以在线程之间和networking客户端之间使用。

你可以在你的FIFO上做一些管道工作,让它们分裂成多个消费者,或者结合多个生产者的消息。 (基本上只是一个FIFO的列表,就像它只是一个大消息que)。

为您的线程使用观察者模式。 给每个观察员一个专门的问题。 您可以将线程和远程客户端的线程基本上相同,只需通过networking即可。 把你的安全放在networking接收部分。

为你的消息制定一个RPC语言。 什么需要通过networking传输。 同样可以用于线程间的通信。

  • UINT – 消息序列号
  • BYTE – 操作码
    • 设置int / uint。
    • 设置string。
    • 设置浮动。
    • 设置数组。
    • 添加对象到列表。 (我们踩一个列表作为一个属性,它可以让你添加对象到一个关卡)。 你在这里创建新的对象还是单独的。
    • 从列表中删除对象。
    • 请求平(所​​以我们可以计算滞后)
    • 发送Ping响应(Pong)。
  • UINT – 属性ID
  • DATA – 属性值,长度取决于types。 一些types在开始时需要附加它们的长度。
  • 校验?

你可以有特殊的操作码来设置玩家的名字或聊天中的一些文字,但是除此之外,你可以给玩家所有的服务器所看到的特殊的string属性。 播放器设置string,服务器发送chatmessage / namechange请求。 实际上,使用这种方法,你或许可以把上面的东西分解成更简单的东西,那里的networking代码就是在服务器上设置一些特殊的状态对象。 你会有SetRawDara,LengthOfData,Id。 并且浮点数,整数,string,数组等都将以相同的方式处理。 只要把它留给服务器来检查它的有效性(播放器是否拥有该对象,数据大小是否正确),并找出如何处理它。

你可能想要使用TCP和UDP。 对于像玩家位置的UDP很好。 像聊天或传输初始状态的东西需要可靠性。

对于线程 – >线程通信,你可能实际上需要一个不同的RPC。 你实际上可能能够使用lambda函数的列表,并完全抛弃协议/parsing(在C ++ 11上这应该是可能的,不知道C#)。 或者使用函数指针。 如果你这样做networking的东西需要有点分离,但这可能是最好的。 你不需要线程 – >线程中的安全所有权的东西,如果源是内部的,你可以相信所设置的数据的大小是理智的。

每个线程/客户端在调用其更新函数之前(或之后)检查队列。 如果有新消息,则查看操作

服务器线程有一个游戏世界状态。 它包含每一个对象。 当它拥有的一个属性被更新时,它将通过FIFO将它传送给所有监听(或范围内的)'客户'线程。 它将听取每个客户的问题列表。 它的循环基本上是:

  • 检查传入的线程。
  • 处理传入的消息(这应该是一个已合并所有传入客户端消息的FIFO),根据需要更新值。
  • 执行自己的更新。
  • 对于每一个更新的值(来自需要向其他人广播的传入线程(除非你正在做P2P),或者内部更新被添加到要广播给所有客户端的事物列表中。
  • 向所有监听客户端发送广播消息。

想想看,研究什么玩家听某个特定的“财产”可能会有点难,因为健康财产不会有任何对象本身或在太空的概念,所以也许可以确保每个财产家长参考。

networking线程。 无论是全球一个,每个连接一个或处理networking数据包的池。 这对于客户端和服务器都是一样的。

  • 听一个包。
  • 检查数据包的有效性
    • 客户是否拥有要改变的财产ID?
    • 尺寸是否正确
  • 如果有效,将其转换为内部消息表示(函数指针,lambda函数,操作码)。 将其添加到此客户端的FIFO队列中。

客户端线程:

  • 处理来自服务器的消息(可能来自其他客户端,如果你在做P2P)。
  • 更新属性
  • 如果使用单独的渲染线程发送相关的消息(即只是可见的信息,位置,在视图中的任何新的对象)

渲染线程(这也可以是声音):

  • 接收和处理消息。
  • 渲染。

物理线程有点复杂。 我的建议是确保你的物理引擎工作的可预测性,所有的客户将得到一个相同的结果。 而不是传输连续的位置更新stream,而是传输基本的物理信息(总体施加的力,速度,当前位置),并让客户端解决。