深刻了解Swift并发性的细节,发现Swift如何在提升性能的同时提供更大的安全性,避免数据竞争和线程爆炸。咱们将探讨Swift任务与Grand Central Dispatch有何不一样,新的合做线程模型如何工做,以及如何确保你的应用程序得到最佳性能。为了得到本次会议的最大收获,咱们建议首先观看 "Meet async/await in Swift"、"Explore structured concurrency in Swift"和 "Protect mutable state with Swift actors"。数据库
今天,咱们和大家谈谈有关Swift并发的一些基本细微差异。这是一个高级讲座,它创建在早期关于Swift并发性的一些讲座之上。若是你不熟悉async/await、结构化并发和Actor的概念,我鼓励你先看一下其余的讲座。在以前关于Swift并发性的讲座中,你已经了解了今年Swift原生的各类语言特性以及如何使用它们。在此次讲座中,咱们将深刻了解为何这些基元是这样设计的,不只是为了语言安全,也是为了性能和效率。当你在本身的应用中尝试和采用Swift并发时,咱们但愿此次讲座能给你一个更好的心理模型,让你了解如何推理Swift并发,以及它如何与Grand Central Dispatch等现有线程库对接。数组
咱们今天要讨论几件事。缓存
首先,咱们将讨论 Swift 并发背后的线程模型,并将其与 Grand Central Dispatch 进行对比。咱们将谈论咱们如何利用并发语言的特性,为Swift创建一个新的线程池,从而实现更好的性能和效率。安全
最后,在这一节中,咱们将谈谈在将代码移植到使用Swift并发性时须要注意的事项。markdown
而后,将谈论Swift并发中经过Actor进行的同步。 咱们将讨论Actor是如何工做的,它们与你可能已经熟悉的现有同步基元(如串行调度队列)相好比何,网络
最后,在使用行为体编写代码时须要注意的一些事项。数据结构
今天咱们有不少内容要讲,因此让咱们直接开始吧。多线程
在咱们今天关于线程模型的讨论中,咱们将首先看一下用当今可用的技术(如Grand Central Dispatch)编写的一个示例应用程序。而后,咱们将看看一样的应用程序在用Swift并发性重写时的表现。并发
假设我想写我本身的新闻源阅读器应用。异步
让咱们来谈谈个人应用程序的高级组件是什么。
个人应用程序将有一个主线程,用于驱动用户界面。
我将有一个数据库来跟踪用户订阅的新闻源,最后还有一个子系统来处理网络逻辑,以便重新闻源中获取最新内容。
让咱们考虑一下如何用Grand Central Dispatch队列来构造这个应用程序。
咱们假设用户要求查看最新的新闻。
在主线程上,咱们将处理用户事件的手势。
从这里,咱们将把请求异步分派到一个处理数据库操做的串行队列上。
这样作的缘由有两个方面。
首先,经过将工做分配到不一样的队列,咱们确保主线程即便在等待潜在的大量工做发生时也能保持对用户输入的响应。
其次,对数据库的访问是受保护的,由于一个串行队列保证了相互排斥。
在数据库队列中,咱们将遍历用户订阅的新闻源,并为每一个新闻源安排一个网络请求到咱们的URLSession,如下载该源的内容。
当网络请求的结果出现时,URLSession的回调将在咱们的委托队列中被调用,该队列是一个并发的队列。在每一个结果的完成处理程序中,咱们将同步更新数据库中每一个feeds的最新请求,以便缓存起来供未来使用。最后,咱们会唤醒主线程来刷新用户界面。
这彷佛是构造这样一个应用程序的一个彻底合理的方式。咱们已经确保了在处理请求时不会阻塞主线程。经过并发地处理网络请求,咱们已经利用了程序中固有的并行性。让咱们仔细看看一个代码片断,它显示了咱们如何处理网络请求的结果。
首先,咱们建立了一个URLSession,用于执行重新闻源的下载。正如你在这里看到的,咱们已经将这个URLSession的委托队列设置为一个并发队列。
而后咱们遍历全部须要更新的新闻源,并为每一个新闻源在URLSession中安排一个数据任务。在数据任务的完成处理程序中--它将在委托队列中被调用--咱们对下载的结果进行反序列化,并将其格式化为文章。
而后,在更新feed的结果以前,咱们针对咱们的数据库队列进行同步调度。
因此在这里你能够看到,咱们写了一些线性代码来作一些至关直接的事情,但这段代码有一些隐藏的性能陷阱。
为了进一步了解这些性能问题,咱们须要首先深刻了解线程是如何处理GCD队列的工做的。
在Grand Central Dispatch中,当工做被排入一个队列时,系统会调出一个线程来处理该工做项目。
因为一个并发队列能够同时处理多个工做项目,系统会启动多个线程,直到咱们的全部CPU核心都达到饱和。
然而,若是一个线程阻塞了--就像在这里的第一个CPU核上看到的那样--而且在并发队列上还有更多的工做要作,GCD将带起更多的线程来耗尽剩余的工做项目。
这样作的缘由有两个方面。
首先,经过给你的进程提供另外一个线程,咱们可以确保每一个核心在任什么时候候都有一个执行工做的线程。这给你的应用程序提供了一个良好的、持续的并发性水平。
其次,被阻塞的线程可能正在等待一个资源,如信号量,而后才能取得进一步进展。被带入队列继续工做的新线程可能可以帮助解开被第一个线程等待的资源。
如今咱们对GCD中的线程提高有了更多的了解,让咱们回头看看咱们的新闻应用中的CPU执行代码。
在像Apple Watch这样的双核设备上,GCD首先会带出两个线程来处理新闻更新结果。 当这些线程在访问数据库队列时受阻,更多的线程被建立以继续处理网络队列。 而后,CPU必须在处理网络结果的不一样线程之间进行上下文切换,如不一样线程之间的白色垂直线所示。
这意味着在咱们的新闻应用中,咱们很容易就会出现很是多的线程。 若是用户有一百个Feeds须要更新,那么当网络请求完成后,每一个URL数据任务都会在并发队列中有一个完成块。 当每一个回调在数据库队列上阻塞时,GCD会带出更多的线程,致使应用程序有不少线程。
如今你可能会问,在咱们的应用程序中拥有大量的线程有什么很差?在咱们的应用程序中拥有大量的线程,意味着系统对线程的过分承诺超过了咱们的CPU核心。
考虑一个有六个CPU核心的iPhone。 若是咱们的新闻应用程序有一百个须要处理的feed更新,这意味着咱们已经用比核心多16倍的线程对iPhone进行了过分配置。这就是咱们所说的线程爆炸现象。 咱们以前的一些WWDC讲座已经进一步详细介绍了与此相关的风险,包括在你的应用程序中出现死锁的可能性。 线程爆炸还伴随着内存和调度的开销,这些开销可能不会当即显现出来,因此让咱们进一步研究一下。
回顾一下咱们的新闻应用,每一个被阻塞的线程在等待再次运行时,都在抓着宝贵的内存和资源。
每一个被阻塞的线程都有一个堆栈和相关的内核数据结构来跟踪该线程。其中一些线程可能持有其余正在运行的线程可能须要的锁。这对于没有进展的线程来讲,是大量的资源和内存的占用。 因为线程爆炸,也有更大的调度开销。 随着新线程的出现,CPU须要进行全线程上下文切换,以便从旧线程切换到开始执行新线程。 当被阻塞的线程再次变得可运行时,调度员必须在CPU上对线程进行分时,以便它们都能取得进展。
如今,若是这种状况只发生几回,线程的分时是没有问题的--这就是并发的力量。 可是,当出现线程爆炸时,必须在一个核心有限的设备上分时共享数百个线程,会致使过分的上下文切换。这些线程的调度延迟超过了它们会作的有用工做的数量,所以,致使CPU的运行效率也下降。
正如咱们到目前为止所看到的,在使用GCD队列编写应用程序时,很容易错过一些关于线程卫生的细微差异,从而致使性能不佳和更大的开销。
基于这一经验,Swift在设计语言中的并发性时采起了不一样的方法。 咱们在构建 Swift 并发时也考虑到了性能和效率,所以你的应用程序能够享受到可控的、结构化的、安全的并发。 有了Swift,咱们想把应用程序的执行模式从下面这种有不少线程和上下文切换的模式改成这样。
在这里你能够看到,咱们的双核系统上只有两个线程在执行,并且没有线程上下文切换。 咱们全部被阻塞的线程都消失了,取而代之的是一个被称为continuation的轻量级对象来跟踪工做的恢复状况。 当线程在Swift并发下执行工做时,它们会在**cont.**之间进行切换,而不是执行完整的线程上下文切换。 这意味着咱们如今只须要支付函数调用的成本。
所以,咱们但愿Swift并发的运行时行为是,只建立与CPU核心数量相同的线程,而且线程在被阻塞时可以廉价、高效地在工做项目之间切换。咱们但愿你能写出容易推理的线性型代码,并为你提供安全、可控的并发性。
为了实现咱们所追求的这种行为,操做系统须要一个运行时契约,即线程不会阻塞,而这只有在语言可以为咱们提供这种契约时才有可能。 所以,Swift的并发模型和围绕它的语义在设计时就考虑到了这个目标。为此,我想深刻探讨一下Swift语言层面的两个特色,它们使咱们可以与运行时保持契约关系。
第一个是来自 await 的语义,第二个是来自 Swift 运行时对任务依赖关系的跟踪。
让咱们在新闻应用的例子中考虑这些语言特性。
这是咱们以前走过的代码片断,它处理了咱们的新闻提要更新的结果。 让咱们看看这个逻辑在用Swift并发原语编写时是什么样子。
咱们首先会建立一个辅助函数的异步实现。 而后,咱们不在并发调度队列中处理网络请求的结果,而是在这里使用一个任务组来管理咱们的并发性。 在任务组中,咱们将为每一个须要更新的feed建立子任务。 每一个子任务将使用共享的URLSession从Feed的URL上执行下载。 而后,它将反序列化下载的结果,将其格式化为文章,最后,咱们将调用一个异步函数来更新咱们的数据库。 在这里,当调用任何异步函数时,咱们用一个await关键字来注释它。
从 "Meet async/await in Swift "讲座中,咱们了解到await是一个异步等待。也就是说,在等待异步函数的结果时,它不会阻塞当前线程。相反,该函数可能被暂停,线程将被释放出来执行其余任务。
这种状况是如何发生的?如何放弃一个线程呢?个人同事Varun如今将阐明在Swift运行时的引擎盖下是如何实现这一目标的。
在讨论异步函数是如何实现的以前,咱们先快速回顾一下非异步函数的工做原理。
在一个正在运行的程序中,每一个线程都有一个堆栈,它用来存储函数调用的状态。
如今让咱们专一于一个线程。
当线程执行一个函数调用时,一个新的栈帧被推到它的堆栈上。
这个新建立的堆栈帧能够被函数用来存储局部变量、返回地址和任何其余须要的信息。
一旦函数执行完毕并返回,它的堆栈帧就被弹出。
如今咱们来考虑一下异步函数。
假设一个线程从updateDatabase函数中调用了一个关于Feed类型的add(:)方法。 在这个阶段,最近的堆栈帧将是为add(:)。
这个栈帧存储了不须要跨越暂停点的局部变量。 add(_:)的主体有一个暂停点,用 await 标记。 本地变量,id和article,在被定义后当即在for循环的主体中使用,中间没有任何暂停点。因此它们将被存储在这个栈帧中。
此外,堆上将有两个异步调用帧,一个用于updateDatabase,一个用于add。异步调用帧存储的信息确实须要在各暂停点之间可用。
请注意,newArticles参数是在await以前定义的,但须要在await以后才可用。
这意味着add的异步调用帧 将保持对newArticles的跟踪。
假设该线程继续执行。
当save函数开始执行时,add的堆栈帧被save的堆栈帧所取代。
不是添加新的堆栈帧,而是替换最上面的堆栈帧,由于任何将来须要的变量都已经存储在异步调用帧的列表中了。
保存函数也得到了一个异步调用帧供其使用。
当文章被保存到数据库时,若是线程能作一些有用的工做而不是被阻塞,那就更好了。
假设保存函数的执行被暂停。而线程被从新使用来作一些其余有用的工做,而不是被阻塞。
由于全部跨越暂停点的信息都存储在堆上,因此能够用来在之后的阶段继续执行。
这个异步调用帧的列表是一个Continuation的运行时表示。
假设过了一下子,数据库请求完成了,假设一些线程被释放出来。
这多是与以前相同的线程,也多是一个不一样的线程。
一旦它执行完毕并返回一些ID,那么save的堆栈帧将再次被add的堆栈帧所取代。
以后,该线程能够开始执行zip。
对两个数组进行压缩是一个非异步操做,因此它将建立一个新的堆栈帧。
因为Swift继续使用操做系统堆栈,异步和非异步的Swift代码均可以有效地调用到C和Objective-C。
此外,C和Objective-C代码能够继续有效地调用非异步的Swift代码。
一旦zip函数完成,它的堆栈帧将被弹出,执行将继续。
到目前为止,我已经描述了await是如何设计的,以确保高效的暂停和恢复,同时释放线程的资源来作其余工做。
如前所述,一个函数能够在一个等待点(也被称为潜在的暂停点)被分解成Continuations。
在这种状况下,URLSession数据任务是异步函数,它以后的剩余工做是Continuations。 只有在异步函数完成后,才能执行Continuations。
这是一个由Swift并发运行时跟踪的依赖关系。
一样,在任务组中,一个父任务可能会建立几个子任务,每一个子任务都须要在父任务进行以前完成。 这是一种依赖关系,在你的代码中经过任务组的范围来表达,所以明确地被Swift编译器和运行时所知。 在Swift中,任务只能等待Swift运行时已知的其余任务--不管是Continuations仍是子任务。
所以,当用Swift的并发原语构建代码时,运行时会清楚地了解任务之间的依赖链。
到目前为止,你已经了解到Swift的语言特性是如何容许任务在等待过程当中被暂停的。
相反,执行线程可以对任务的依赖性进行推理,并接上一个不一样的任务。
这意味着用Swift并发性编写的代码能够维持一个运行时契约,即线程老是可以取得进展。
咱们已经利用这个运行时契约,为Swift并发性创建了集成的操做系统支持。
这是以一个新的合做线程池的形式,支持Swift并发做为默认执行器。
新的线程池将只产生与CPU内核相同数量的线程,从而确保不对系统进行过分承诺。
与GCD的并发队列不一样,当工做项目受阻时,会产生更多的线程,而Swift的线程老是能够向前推动。所以,默认运行时能够明智地控制线程的生成数量。
这让咱们能够给你的应用程序提供你须要的并发性,同时确保避免过分并发的已知陷阱。
在之前关于Grand Central Dispatch并发性的WWDC讲座中,咱们曾建议你将你的应用程序结构化为不一样的子系统,并在每一个子系统中保持一个串行调度队列,以控制你的应用程序的并发性。 这意味着你很难在一个子系统内得到大于1的并发性,而不会有线程爆炸的风险。
在Swift中,语言为咱们提供了强大的不变性,运行时利用了这些不变性,从而可以在默认运行时中透明地为你提供更好的控制并发性。
如今你对Swift并发的线程模型有了更多的了解,让咱们来看看在你的代码中采用这些使人兴奋的新功能时要注意的一些问题。
你须要记住的第一个考虑因素与将同步代码转换为异步代码时的性能有关。 早些时候,咱们谈到了一些与并发性相关的成本,如Swift运行时的额外内存分配和逻辑。 所以,你须要注意的是,只有当在代码中引入并发性的成本超过了管理并发性的成本时,才会用Swift并发性编写新的代码。
这里的代码片断实际上可能并无从催生一个子任务的额外并发性中获益,只是为了从用户的默认值中读取一个值。这是由于子任务所作的有用工做被建立和管理任务的成本削弱了。 所以,咱们建议在采用Swift并发时,用仪器系统跟踪对你的代码进行分析,以了解它的性能特征。
第二件须要注意的事情是围绕await的原子性概念。
Swift并不保证在await以前执行代码的线程也是将接续的线程。
事实上,await在你的代码中是一个明确的点,代表原子性被打破了,由于任务可能会被自愿取消调度。
所以,你应该注意不要在等待中持有锁。
一样地,线程特定的数据也不会在await中被保留下来。
你的代码中任何指望线程定位的假设都应该被从新审视,以考虑到 await 的暂停行为。
最后,最后的考虑与运行时契约有关,它是Swift中高效线程模型的基础。
回顾一下,在Swift中,语言容许咱们坚持一个运行时契约,即线程老是可以向前推动。
正是基于这一契约,咱们创建了一个合做线程池,做为Swift的默认执行器。
当你采用 Swift 并发时,必须确保在你的代码中也继续维护这一契约,以便合做线程池可以以最佳方式运行。
经过使用安全的基元,使你的代码中的依赖关系明确化和已知化,就有可能在合做线程池中保持这种契约。
有了Swift并发原语,好比await、actors和任务组,这些依赖关系在编译时就已经被知道了。所以,Swift编译器会强制执行这一点,并帮助你保留运行时契约。
像os_unfair_locks和NSLocks这样的原语也是安全的,但在使用它们时须要谨慎。在同步代码中使用锁是安全的,当用于围绕一个紧密的、众所周知的关键部分进行数据同步时。这是由于持有锁的线程老是可以在释放锁的过程当中取得进展。所以,虽然该基元可能会在竞争中阻断线程一小段时间,但它并不违反向前推动的运行时契约。值得注意的是,与Swift并发原语不一样,没有编译器支持来帮助正确使用锁,因此正确使用这一原语是你的责任。
另外一方面,像semaphores和条件变量这样的基元,在Swift并发中使用是不安全的。这是由于它们向Swift运行时隐藏了依赖性信息,但在你的代码中执行时却引入了依赖性。因为运行时不知道这种依赖关系,因此它没法作出正确的调度决策并解决这些问题。特别是,不要使用建立非结构化任务的原语,而后经过使用信号量或不安全的原语,追溯性地引入跨任务边界的依赖关系。这样的代码模式意味着一个线程能够无限期地阻塞信号量,直到另外一个线程可以解除阻塞。这违反了线程向前推动的运行时契约。
为了帮助你识别代码库中这种不安全基元的使用,咱们建议用如下环境变量测试你的应用程序。这将在修改过的调试运行时下运行你的应用程序,该运行时强制执行向前推动的不变量。 这个环境变量能够在Xcode中设置在你的项目方案的Run Arguments窗格中,如图所示。
当用这个环境变量运行你的应用程序时,若是你看到一个来自合做线程池的线程彷佛被挂起,这代表使用了一个不安全的阻塞原语。
如今,在了解了线程模型是如何为Swift并发性设计的以后,让咱们再来了解一下在这个新世界中可用于同步状态的基元。
在关于Actor的Swift并发性讲座中,你已经看到了Actor是如何被用来保护易变的状态不被并发访问的。
换句话说,Actor提供了一个强大的新同步原语,你可使用。
回顾一下,Actor保证了相互排斥:一个Actor在同一时间最多只能执行一个方法调用。相互排斥意味着Actor的状态不会被同时访问,从而防止数据竞争。
让咱们看看Actor与其余形式的互斥相好比何。
考虑一下前面的例子,经过同步到一个串行队列来更新数据库中的一些文章。 若是队列尚未运行,咱们就说不存在竞争。 在这种状况下,调用线程被重用来执行队列上的新工做项目,而没有任何上下文切换。 相反,若是序列队列已经在运行,则称该队列处于争用状态。 在这种状况下,调用线程会被阻塞。 这种阻塞行为就是以前演讲中早先描述的引起线程爆炸的缘由。 锁也有这种行为。
因为与阻塞有关的问题,咱们一般建议你最好使用Dispatch async。 Dispatch async的主要好处是它是无阻塞的。 所以,即便在争用的状况下,它也不会致使线程爆炸。 在串行队列中使用Dispatch async的缺点是,当没有竞争的时候,Dispatch须要请求一个新的线程来作异步工做,而调用线程则继续作其余事情。 所以,频繁使用Dispatch async会致使过多的线程唤醒和上下文切换。
这就给咱们带来了Actor。
Swift的Actor利用合做线程池的优点进行有效的调度,从而结合了这两个世界的优势。 当你在一个没有运行的Actor上调用一个方法时,调用线程能够被重用来执行方法调用。 在被调用的Actor已经在运行的状况下,调用线程能够暂停它正在执行的函数,并接上其余工做。
让咱们看看这两个属性在新闻应用的例子中是如何工做的。 咱们来关注一下数据库和网络子系统。
当更新应用程序以使用Swift并发时,数据库的串行队列将被一个数据库Actor所取代。 网络的并发队列能够被每一个新闻源的一个Actor所取代。为了简单起见,我在这里只展现了三个Actor--体育Actor、天气Actor和健康Actor--但在实践中,会有更多的Actor。 这些Actor将在合做线程池中运行。 feedActor与数据库互动,以保存文章和执行其余动做。 这种互动涉及到从一个Actor到另外一个Actor的执行切换。
咱们称这个过程为Actor跳转。 让咱们来讨论一下Actor跳转是如何进行的。
假设体育频道的Actor在合做线程池中的一个线程上运行,它决定将一些文章保存到数据库中。
如今,让咱们考虑数据库没有被使用。 这是不存在竞争的状况。
线程能够直接从体育频道的Actor跳到数据库的Actor。
这里有两件事须要注意。 首先,线程在跳转Actor时没有阻塞。 第二,跳转不须要不一样的线程;运行时能够直接暂停体育节目Actor的工做项目,为数据库Actor建立一个新的工做项目。
假设数据库Actor运行了一段时间,但它尚未完成第一个工做项。在这个时候,假设天气预报Actor试图在数据库中保存一些文章。
这就为数据库Actor创造了一个新的工做项目。Actor经过保证相互排斥来确保安全;在给定的时间内,最多只有一个工做项是活动的。 因为已经有一个活动的工做项目D1,新的工做项目D2将被保留。
Actor也是无阻塞的。在这种状况下,天气预报Actor将被暂停,它所执行的线程如今被释放出来作其余工做。
过了一下子,最初的数据库请求完成了,因此数据库Actor的活动工做项被移除。
在这一点上,运行时能够选择开始执行数据库Actor的未决工做项目。 或者它能够选择恢复一个进位Actor。 或者它能够在被释放的线程上作一些其余工做。
当有不少异步工做,特别是有不少争论时,系统须要根据什么工做更重要来进行权衡。 理想状况下,高优先级的工做,如涉及用户互动的工做,将优先于后台工做,如保存备份。 因为重入的概念,Actor被设计成容许系统很好地安排工做的优先次序。 可是为了理解为何重入性在这里很重要,让咱们先看看GCD是如何处理优先级的。
考虑一下带有串行数据库队列的原始新闻应用。 假设数据库收到了一些高优先级的工做,好比获取最新数据以更新用户界面。 它也会收到低优先级的工做,例如将数据库备份到iCloud。 这须要在某个时间点完成,但不必定是当即完成。 随着代码的运行,新的工做项目被建立并以某种交错的顺序添加到数据库队列中。 Dispatch Queue以严格的先入先出顺序执行收到的项目。 不幸的是,这意味着在项目A执行完后,在进入下一个高优先级项目以前,须要执行五个低优先级的项目。 这就是所谓的优先级倒置。
串行队列经过提升队列中全部在高优先级工做以前的工做的优先级来解决优先级倒置的问题。 在实践中,这意味着队列中的工做将更快完成。 然而,这并无解决主要问题,即在项目B开始执行以前,项目1到5仍然须要完成。 解决这个问题须要改变语义模型,使其脱离严格的先进先出。
这就把咱们带到了Actor的重入。 让咱们经过一个例子来探索重入是如何与排序相联系的。
考虑一下在一个线程上执行的数据库Actor。 假设它被暂停,等待一些工做,而体育节目的Actor开始在该线程上执行。 假设过了一下子,体育频道的Actor调用数据库Actor来保存一些文章。 因为数据库Actor是未被征用的,线程能够跳到数据库Actor上,尽管它有一个待处理的工做项目。 为了执行保存操做,将为数据库Actor建立一个新的工做项。
这就是actor reentrancy的含义;当一个Actor上的一个或多个旧的工做项被暂停时,该Actor上的新工做项能够取得进展。 Actor仍然保持相互排斥:在一个给定的时间内最多只能有一个工做项在执行。
一段时间后,项目D2将完成执行。 注意,D2在D1以前完成了执行,尽管它是在D1以后建立的。 所以,对Actor重入的支持意味着Actor能够按照不是严格意义上的先入先出的顺序执行项目。
让咱们再来看看以前的例子,但要用一个数据库Actor而不是一个序列队列。 首先,工做项目A将被执行,由于它有很高的优先级。 一旦执行完毕,就会出现和以前同样的优先级倒置。
因为Actor是为重入设计的,运行时能够选择将优先级较高的项目移到队列的前面,排在优先级较低的项目前面。 这样一来,较高优先级的工做就能够先执行,较低优先级的工做则在后面。 这直接解决了优先级倒置的问题,容许更有效的调度和资源利用。 我已经谈到了一些关于使用合做线程池的Actor是如何被设计来维持相互排斥和支持有效的工做优先级的。
还有一种Actor,即MainActor,其特色有些不一样,由于它抽象了系统中的一个现有概念:主线程。
考虑一下使用Actor的新闻应用程序的例子。
当更新用户界面时,你须要对MainActor进行调用,也须要从MainActor进行调用。 因为主线程与合做线程池中的线程是不相干的,这须要进行上下文切换。 让咱们经过一个代码例子来看看这其中的性能影响。 考虑下面的代码,咱们在MainActor上有一个函数updateArticles,它从数据库中加载文章,并为每篇文章更新UI。
循环的每一次迭代都须要至少两次上下文切换:一次是从MainActor跳到数据库Actor,另外一次是跳回来。 让咱们看看这样一个循环的CPU使用率是怎样的。
因为每一个循环迭代都须要两次上下文切换,因此会出现一个重复的模式,即两个线程在短期内相继运行。 若是循环迭代的数量很少,并且每次迭代都在作大量的工做,这多是正确的。
然而,若是执行过程当中频繁地在MainActor上跳来跳去,切换线程的开销就会开始增长。 若是你的应用程序在上下文切换中花费了大量的时间,你应该重组你的代码,使MainActor的工做被分批进行。
你能够经过把循环推到loadArticles和updateUI方法调用中,确保它们处理数组而不是一次处理一个值来分批工做。 分批工做能够减小上下文切换的次数。 虽然在合做线程池上的Actor之间跳转很迅速,但在编写应用程序时,你仍然须要注意与MainActor之间的跳转。
回顾过去,在此次演讲中,你已经了解到咱们是如何努力使系统达到最高效的,从合做线程池的设计--非阻塞等待机制--到如何实现Actor。 在每一个步骤中,咱们都在使用运行时契约的某些方面来提升你的应用程序的性能。 咱们很高兴看到你如何使用这些使人难以置信的新语言特性来编写清晰、高效和使人愉悦的Swift代码。 谢谢你的观看,祝你有一个美好的WWDC。