微软MR技术专家分享:八年经验和技能AR/VR多线程

导读多线程是指从软件或者硬件实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。微软混合现实技术专家贾里德

多线程(Multithreading)是指从软件大概硬件实行多个线程并发实行的本领。具备多线程本领的计划机因有硬件扶助而不妨在同偶尔间实行多于一个线程,从而提高完全处置本能。微软搀和实际本领大师贾里德·拜恩兹(Jared Bienz)是一位驰名的软件框架结构师,有着20多年的从业体味。日前,拜恩兹撰文瓜分了本人在AR/vr/MR多线程处置方面包车型的士八年体味和本领。底下是华夏AI网的简直整治:

 微软MR技术专家分享:八年经验和技能AR/VR多线程

要精确实行多线程并遏制易,但它对于资源受限的挪动摆设流利运转模仿至关要害。在服务于微软的生存中,我偶尔机在四年多的功夫里扶助协调搭档为HoloLens编写高本能的运用步调。我其余有4年多的功夫扶助协调搭档为智高手提式无线电话机宁靖板电脑编写高本能运用步调。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

我早已蓄意撰写这篇作品。这基础上是我对AR/VR/MR模仿的多线程处置的8年体味瓜分。固然本文重要关心Unity和C#,但我蓄意个中引荐的观念仍旧不妨为一切谈话和平运动转时的模仿开拓者带来价格。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

1. 什么是线程?

 微软MR技术专家分享:八年经验和技能AR/VR多线程

我领会这是一个基础性的题目,但我从它发端写起是有一个要害因为。这个来由会在本章反面变得明显起来。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

维基百科将线程刻画为:不妨与其它指令并发实行的一系列指令。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

我夸大并发实行是由于它对这次计划至关要害。并发运转多个工作的本领使得线程对于模仿至关要害。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

2. 对于内核与线程的扼要证明

 微软MR技术专家分享:八年经验和技能AR/VR多线程

一个CPU不妨有多个内核,而有些内核不妨运转多个线程。比方,Ryzen Threadripper最多有64个内核,每个内核不妨运转2个线程。这表示着,即使你编写的模仿属于高度多线程,你大概会有多达128个不同的工作同时爆发。你不妨用这些线程来运转NPC的人为智能大脑,大概在物理模仿中创造碰撞。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

但请记取,大学一年级致本质场景不会逼近128个线程。固然是英特尔的旗舰i9 10900k都不过供给20个并发线程。但是,编写多线程代码表示着供给多个内核的摆设不妨同时爆发多个工作。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

3. 线程何如感化运用步调

 微软MR技术专家分享:八年经验和技能AR/VR多线程

固然你不依附进步的人为智能,但简直一切的MR运用都在某种程度上运用物理。比方,Hand Menu菜单中的按钮会运用物理来检验和测定指尖何时交战按钮的表面。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

但远比物理更要害的是衬托。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

简直一切的玩耍引擎(包括Unity)都仍旧依附于单线程进行衬托。没错,惟有一个线程不妨在屏幕上绘制。固然是超底层的Directx API都只扶助在扶助线程上列队吩咐。关系吩咐仍旧须要发送到衬托线程进行绘制。这是一个特其他线程。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

正如你不妨设想的那样,从衬托线程获得代码不妨开释引擎以绘制实质。你将博得更高的帧速度,看到更少的卡顿和频闪。你的运用步调会发觉更加高响(高相应速率)和宁静。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

4. 好吧,以是不要在Render Thread运转代码吗?

 微软MR技术专家分享:八年经验和技能AR/VR多线程

这听起来明显像是在隐藏,不是吗?但究竟表明,Render Thread是一切代码运转的默许场所。不只如许,在Render Thread运转代码是不行遏止的工作。为了证明因为,咱们底下来看看一个基础的Unity立方体。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

使得立方体成为立方体的重要因为之一是称为网格衬托器(Mesh Renderer)的动作。网格衬托器做什么?固然,它绘制立方体。换句话说,为了使一个Unity立方体成为一个立方体,它必需存在于Render Thread之上。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

Unity常常将Render Thread称为干线程、运用线程、以及UI线程。请提防,它们都是同一个道理。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

5. coroutine(协程)与线程

 微软MR技术专家分享:八年经验和技能AR/VR多线程

当Unity开拓者创造coroutine时,大学一年级致人觉得他们仍旧创造了多线程。可惜的是,究竟远非如许。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

Unity遏制一位coroutine的博士指出:coroutine就像一个函数,它不妨休憩实行并将遏制权返回给Unity,但而后会鄙人一个帧中贯穿实行。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

要害的是要认识到coroutine仍旧是在Render Thread上运转。

 微软MR技术专家分享:八年经验和技能AR/VR多线程

设想一下一个大略的Unity运用步调在如许的轮回中运转:

 微软MR技术专家分享:八年经验和技能AR/VR多线程

即使动作A启用两个coroutine,则轮回将大略地变动为:

coroutine和正则函数的独一辨别在于,coroutine的一限制不妨在帧之间挂起。挂起时会包括存在重要字yield的任何行。固然这大概会腾出功夫让其余工作运转,但编写蹩脚的coroutine仍旧特出简单给Render Thread形成宏大的负载。

coroutine特殊:你领会在coroutine展现特殊会爆发什么吗?大概不是你设想的那样。特殊不会遏止运用步调,以至不会禁止使用Behavior。独一爆发的工作是,coroutine从革新轮回中unscheduled。Behavior不会提防到缺陷,以至不领会coroutine仍旧被unscheduled。

因为coroutine不是并发运转,以是最佳把它看作是一个功夫切片机制。它们不是真实的多线程。

6. Thread.Start又何如?

咱们毕竟聊到多线程的第一个本质采用。System.Threading.Thread本质上代表一个线程,而调用Thread.Start将启发工作在所述线程并发运转。

但对于Thread类,你须要领会Thread类的实例表白一个不妨实行处事的东西,而不是乞求实行处事。很多函数不妨安置在Thread上运转,而等候Thread实行并纷歧定表示着函数成功实行。比方,特殊大概会爆发。

恰是因为这些因为,通用Windows Platform(HoloLens运转的平台)以至不包括System.Threading.Thread。差异,UWP供给了一种名为ThreadPool的元素,个中各个处事变不妨进行scheduled。

在本文中,我不安排计划Thread或ThreadPool,由于我蓄意中心计划另一种本领。但是,我保持想大略地讲讲这些题目,由于来日运用Thread的Unity开拓者会因为代码无法为HoloLens编写翻译感触迷惑或懊丧。Thread类大概会被增添到UWP的将来版本中,但我蓄意表明固然它可用,咱们仍旧有更好的形式不妨按照。

7. 回调中的“猫腻”

什么是回调?维基百科将回调界说为:动作参数字传送播给其余代码的任何可实行代码…这个实行…大概会在稍后的异步回调中爆发。

编写“典范”多线程代码的开拓者特出熟习回调,由于一旦你发端并发运转代码,不知何以你须要领会它是于何时实行。

底下是少许对于回调何如处事的伪代码:

但即使代码长久都没有实行呢?即使由于文献被锁定或数据破坏而在第9行激励特殊何如办呢?

回调长久不会被调用。

即使没有特出的编码,运用步调将长久不会领会爆发了缺陷。就运用步调所知,LoadData已成功运转。这是由于特殊没有爆发在LoadData中,而是爆发在LoadData创造的线程中。

对于考查编写和调节和测试多线程代码的开拓者来说,不遏止(orphan)的回调从来是苦楚的基础。简而言之,这是由于乞求、处事和截止是实足辨别的。

回调与事变:请提防,回调形式偶尔不妨动作事变实行。Azure Spatial Anchors在探求锚点时会实行这一操纵。运用步调调用CreateWatcher发端探求,当找到锚及时,截止将经过AnchorLocated事变传回。这偶尔会启发预见不到的情景。比方,即使在功效器上废除了锚定,则AnchorLocated事变将以NotLocateDanchordesNotExist的状况触发。其余,即使爆发搜集缺陷,运用步调不会领会,除非它同时订阅了Error事变。我并不是说这是一个蹩脚的安排(见下文),但明显,成功地运用鉴于事变的回调体例须要领会哪些情景会启发哪些事变。

8. 什么是跨线程安排(scheduling)?

让咱们再看看之前的伪代码:

你提防到第12行对loadCompleted的调用本质上是在worker线程中实行的吗?即使咱们想在数据加载后可视化,这会成为一个题目。请记取,loadCompleted是在worker线程上运转的,但咱们只能在Render Thread创造GameObject。这就须要跨线程scheduling。

在Azure Spatial Anchors for Unity示例中,你不妨找到一个名为UnityDispatcher的脚本。UnityDispatcher承诺在任何线程上运转的代码乞求该代码在Render Thread上运转。你以至大概在没蓄意识到的情景下看到了这一点。

以下是onCloudAnchorLocated handler的代码片断:

每当ASA定位到一个锚时,AnchorLocated事变将在worker线程上触发。即使运用步调只需将动静写入日记,则不妨接收这个worker线程。究竟上这是更好的采用。但这个运用步调须要天生一个GameObject或挪动一个现有的GameObject,这两个操纵只能在Render Thread上进行。InvokeOnAppThread表白“我领会我仍旧在一个worker线程,但我须要调用在Render Thread运转的代码”。

9. Unity的跨线程安排

固然一切多线程体例都有本人的scheduling办法,但Unity的本拥有点不凡是。据我所知,Unity没有供给直接的API来安排衬托线程上的处事。他们供给的是一种间接的办法。

UnityDispatcher保持了须要在Render Thread上运转的吩咐的列表。当一个worker线程调用InvokeOnAppThread时,这只会将代码增添到列表中。当运用步调启用时,UnityDispatcher将本人备案为coroutine。而后在每个帧上,UnityDispatcher查看列表中能否有任何实质。即使是如许,所述代码将动作UnityDispatcher的Update例程的一限制实行。

UnityDispatcher没有绑定到Azure空间锚,所以你不妨复制该类并在任何名目中运用它。即使没有ASA,你也不妨从GitHub上的ThreadUtils名目中获得这个类的副本。

10. Task-based Programming

针对C++开拓者的证明:我将要发端计划Task-based Programming。我将引荐一个名为Task的C#,但你不会在C++ / WinRT中找到Task。差异,C++开拓者运用IasyccAct之类的接口,而当从C#调用时,这些接口会自动变换为Task。更多消息请参阅这边。

如上所述,调节和测试多线程代码特出艰巨,由于乞求、处事和截止都是彼此辨别的。但我向你承诺过一个更好的本领,我想此刻是功夫计划它了。

很多开拓者都领会Task-based Programming,但很罕见人真实领会它在幕后的处事道理。Task-based Programming一致了咱们前方计划过的观念,大大减少了多线程代码中堕落的机会。底下咱们来看看Task-based Programming是何如简化线程、回融合跨线程scheduling。

11. Auto Threading

在C#中,每当一个函数被async重要字化装时,咱们报告编写翻译器的是“这个代码不妨在另一个线程上运转”

让咱们来看看将数据存在到文献中的少许伪代码:

在本例中,翻开文献、写入字节和封闭文献都将在worker线程上实行。

这个神秘的worker线程是什么功夫创作出来的呢?它是在运用await操纵符时创造的。

即使咱们的示例运用步调具备以下代码行:

这十分于:

异步函数中的代码真实在新线程上运转。但你的运用步调不须要领会这些细节,也不须要太多地关心。

更妙的是,正如斯蒂芬·图布(Stephen Toub)常常说的:“等候的一个巧妙之处即是它能把你带回从来的场合”。让咱们看看这句话在另一个代码示例中的含意吧:

咱们领会这段代码是从Render Thread发端的,由于它是对按钮点击的相应。以是在第4行与GameObject交互是蓄意义的。但咱们同时领会,第7行的await重要字启用了一个新线程。以是,何如本领与第10行和第11行的GameObjects交互呢?

答案是一个叫做SynchronizationContext的元素。简而言之,不管何时运用await,编写翻译器城市记取在worker线程启用之前有哪个线程正在运转。编写翻译器同时会在worker线程实行后登时处置返回Starting Thread的操纵。是的,await自动处置跨线程scheduling。

要害提醒:await永不加塞。看起来像是await妨害了Starting Thread,但这不过编写翻译器的错觉。await之前的一切实质都是内联运转,并且await之后的一切实质都由安排步调拨运输转。当Task在另一个线程中运转时,Render Thread即是如许保护绘制的。

12. 跟踪处事

正如我在Thread.Start一节指出地那样:线程表白一个不妨实行处事的东西,而不是乞求实行处事。这是Task-based Programming的另一个亮点。任何Task实例本质上都表白一个要实行的处事的乞求。这恰是Task类具有IsCompleted和IsFaulted之类的属性。

13. 数据与特殊

我在上头提到回融合事变不妨在worker线程实行时返回数据。我同时提到了worker线程上的特殊常常表示着回调不运转或事变不触发。Task-based Programming经过将数据动作乞求本人的一限制来处置这个题目。

让咱们来看看沟通的LoadData函数,但咱们将其动作Task而不是回调来实行:

让咱们假如第7行实行少许数据反序列化。大学一年级致功夫,这十足都很好,但偶然咱们的运用步调会翻开一个破坏的文献,第7行会议及展览现一个特殊。请记取,这个特殊是在worker线程上激励的。那么,Starting Thread何如处置这个特殊呢?比你设想的要大略:

当咱们等候一个Task而且该Task成功时,来自该Task的任何数据都将返回到Starting Thread。但是,即使咱们等候一个Task,而且在Task里面爆发了特殊,该特殊将传递回Starting Thread,就像它是内联爆发一律。换句话说,在Task中处置特殊和在任何普遍函数中处置特殊都是一律的。

蓄意大师不妨发端领会为什么Task-based Programming会使多线程变得更简单。Task-based Programming供给了一个简单的一致模子。在这个模子中,乞求、处事和截止都真实地彼此关系。

14. 当废除(Cancellation)特出要害的功夫

在某些情景下,Task大概会运转很长功夫。比方,在慢速搜集左右载大文献时。在这些场景中,使Task变得可废除常常会很有扶助。不妨经过将CancellationToken传播到异步函数来实行。而后,在运转一会后,所述函数不妨在实行更多操纵之前查看Token能否已废除。

底下是所述函数的大概格式:

尽大概一再地查看CancellationToken特出要害,如许不妨赶快废除Task。调用ThrowIfCancellationRequested时,即使Token已被废除,则所有Task以OperationCanceledException阻碍。

既然咱们仍旧看到Task不妨废除,底下咱们来设想一下运用Task的Azure Spatial Anchors:

我并不是倡导ASA该当遏止运用事变而发端运用Task。ASA不妨同时探求多个锚,而ASA从不领会何时(以至能否)定位锚。事变在这种情景下的功效很好,只有你领会什么功夫触发了哪些事变。但是,除了事变除外,增添对Task的扶助不妨扶助简化很多罕见的场景。

15. coroutine保持有一席之地的

既然咱们仍旧领会Task的效率,有人民代表大会概会问为什么咱们要用其余办法编写代码。但请记取,Task在worker线程上运转,而GameObject只存在于Render Thread上。这即是coroutine的意旨地方。

coroutine在Render Thread上运转,但不妨将功夫返回到衬托器。窍门是在yielding之前决定处事量。太少会须要很长功夫,而太多则会启发运用步调没有相应。

让咱们设想一个不妨接受数据并用GameObject可视化的coroutine吧:

为了保护60 FPS,运用步调须要在大概16毫秒内衬托帧。咱们假如咱们的运用步调须要4毫秒来衬托。剩下的12毫秒不妨用来创造GameObject。

即使第10行须要2毫秒,咱们就会剩下6毫秒的容量。不只如许,咱们的运用步调每帧只能创造一个GameObject。

在本例中,更好的实行大概如下所示:

在C#中,%运算符计划余数。以是这边咱们说的是“每6个东西之后,把功夫还给衬托器。”6个东西x每个东西2毫秒=12毫秒(凑巧是咱们的估算)。

明显,这个数字对于每个运用步调而言都是举世无双,而且会跟着功夫的推移而变革。运用步调大概会变得更加搀杂,须要更长的功夫来衬托。大概每个独立的GameObject大概会变得更搀杂,须要更长的功夫来创造。没有神秘的数字。要到达精确的平稳,你须要花功夫领略本能。

16. 将coroutine视作Task

以是coroutine有本人的蛮横之地,但此刻咱们有两种不同的本领来处置长功夫运转的代码。不只如许,除非咱们实行某种回调,否则运用步调将不领会VisualizeRoutine何时实行(咱们仍旧领会回调中的“猫腻”)。即使咱们能把coroutine看成Task来斡旋,那不是很好吗?

有一个名为TaskCompletionSource的类承诺你将任何长功夫运转的过程表白为一个Task。简直如下:

在一个长功夫运转的过程发端时,创造TaskCompletionSource。运用TaskCompletionSource.Task表白长功夫运转的进程。实行后,运用TaskCompletionSource.SetResult返回数据。即使过程遇到缺陷,请运用TaskCompletionSource.SetException来传递特殊。咱们不妨很简单地窜改VisualizationRoutine以接受TaskCompletionSource,并在实行后返回少许数据:

剩下的不过启用coroutine并返回Task的helper函数:

17. coroutine中的特殊处置

即使你提防查看,你大概仍旧提防到上头的coroutine中有一个特出要害的脱漏。即使在第26行之前爆发特殊会爆发什么工作呢?

可惜的是,coroutine不能供给与async沟通的编写翻译器功效。coroutine中没有自动特殊传递,这表示着即使咱们不处置特殊,咱们将以爆发一个不遏止的Task。任多么待Task的代码将长久不会回复。即使你觉得这听起来很像是一个不遏止的回调,你一致精确。

你说:“没题目。我把一切十足都打包到一个try/catch block中。”

大概看起来像如许:

这恰是你要做的工作,除了此刻第24行天生了一个CS1626编写翻译器缺陷。

缺陷CS1626无法在带有catch clause的try block中天生值。

CS1626展现的因为特出搀杂,但你只需领会你不能将try/catch放在任何运用yield的行中。这给咱们留住了两个大概的采用:

在任何非yield行范围安置多个try/catch block。在IEnumerator范围安置try/catch选项1最大略,但并非一切情景下都灵验。比方,你不能将try/catch放在foreach语句范围,由于foreach语句包括一个yield。

但咱们何如实行选项2?常常,IEnumerator直接传播到startRoutine。

可惜的是,工作变得烦恼起来。IEnumerator接口有一个属性和两个函数。咱们必需保证,若任何part-IEnumerator爆发特殊,咱们就将阻碍Task。

为了扶助处置这个题目,我创造了ExceptionSafeRoutine。你不妨在GitHub的AsyncUtils.cs中找到它。ExceptionSafeRoutine接收一个IEnumerator和一个TaskCompletionSource。即使在IEnumerator中激励任何特殊,则在TaskCompletionSource树立该特殊。还有一个扩充本领不妨将任何IEnumerator变换为ExceptionSafeRoutine。

结果,咱们革新Visualization Async以保证Task一直实行:

这种本领的酷炫之处在于,任何特殊城市被传递。固然协程没有try/catch block。这使得coroutine的处事办法就像async一律。咱们独一要记取的是,在发端一个coroutine时增添.WithExceptionHandling。

18. 归纳

即使你看到结果,蓄意你不妨向我瓜分你的办法。你能否学到什么呢?有什么我须要填补大概脱漏的吗?大概你有什么其余更好的计划吗?

免责声明:本文章由会员“马同”发布如果文章侵权,请联系我们处理,本站仅提供信息存储空间服务如因作品内容、版权和其他问题请于本站联系