Swift推出了一个新的类型actor来保护可变状态,使得在并发编程中避免数据竞争,使用关键字可使编译器前置检查潜在的并发风险,写出更健壮的代码。编程
从几个例子看数据竞争以及如何避免数组
当两个独立的线程同时访问相同的数据,而且其中至少有一个访问是写入时,就会发生数据竞赛。缓存
数据竞赛的构造很简单,但倒是出了名的难以调试。这里有一个简单的计数器类,它的一个操做是增长计数器并返回其新值。假设咱们继续前进并尝试从两个并发的任务中增量。根据执行的时间,咱们可能获得1而后2,或者2而后1。这是预料之中的,在这两种状况下,计数器都会被留在一个一致的状态。但因为咱们引入了数据竞赛,若是两个任务都读0写1,咱们也可能获得1和1。安全
或者,若是返回语句发生在两个增量操做以后,甚至是2和2。数据竞赛是出了名的难以免和调试的。服务器
值语义类型的 "let "属性是真正不可变的,因此从不一样的并发任务中访问它们是安全的。试图使用值类型解决数据竞争,把咱们的计数器变成一个结构,使它成为一个值类型。markdown
咱们还必须将增量函数标记为mutating的,使它能够修改值属性。还须要把counter变量改为var,使其成为可变的,这又回到了数据竞争的困境中。闭包
除非使用锁或者串行队列来保证原子性,下面咱们看actor是如何解决的。并发
Actor提供一种共享可改变状态的同步机制。框架
一个Actor有它本身的状态,这个状态与程序的其余部分隔离。访问该状态的惟一途径是经过Actor。异步
只要你经过Actor,Actor的同步机制就会确保没有其余代码在同时访问Actor的状态。这给了咱们与手动使用锁或串行调度队列相同的互斥属性,但对于Actor来讲,这是Swift提供的一个基本保证。
Actor是Swift中的一种新类型。它们提供了与Swift中全部命名类型相同的能力。
它们能够有属性、方法、初始化器、下标,等等。它们能够符合协议,也能够用扩展来加强。
像类同样,它们是引用类型;由于Actor的目的是表达共享的可变状态。
事实上,Actor类型的主要特征是它们将其实例数据与程序的其余部分隔离,并确保对这些数据的同步访问。
咱们又有两个并发的任务试图对同一个计数器进行增量。Actor的内部同步机制确保一个增量调用在另外一个开始以前执行完毕。因此咱们能够获得1和2或者2和1,由于这两个都是有效的并发执行,可是咱们不能两次获得相同的计数或者跳过任何数值,由于Actor的内部同步已经消除了Actor状态的数据竞赛的可能性。
让咱们考虑一下,当两个并发任务都试图同时增长计数器时,实际会发生什么。一个会先到,而另外一个则必须等待轮到。但咱们如何确保第二个任务可以耐心地等待轮到它的Actor呢?Swift有一个这样的机制。每当你从外部与Actor交互时,你都是异步进行的。 若是Actor很忙,那么你的代码就会暂停,这样你运行的CPU就能够作其余有用的工做。 当Actor再次变得空闲时,它将唤醒你的代码--恢复执行--以便调用能够在Actor上运行。
这个例子中的 await 关键字代表,对Actor的异步调用可能涉及到这样一个暂停。让咱们再进一步扩展咱们的反例,增长一个没必要要的缓慢的重置操做。这个操做将值设置为0,而后调用适当次数的增量,使计数器达到新值。 这个resetSlowly方法被定义在计数器Actor类型的扩展中,因此它是在Actor内部。 这意味着它能够直接访问Actor的状态,它就是这样作的,将计数器的值重置为0。
它还能够同步地调用Actor上的其余方法,好比调用increment。 这不须要等待,由于咱们已经知道咱们是在Actor上运行。
这是Actor的一个重要属性。
Actor上的同步代码老是在不被打断的状况下运行到完成。
所以,咱们能够按顺序推理同步代码,而不须要考虑并发对咱们Actor状态的影响。
咱们已经强调了咱们的同步代码是不间断运行的,可是Actor之间或与系统中的其余异步代码之间常常进行交互。让咱们花几分钟的时间来谈谈异步代码和Actor。
它负责从其余服务中下载图像。它还将下载的图像存储在一个缓存中,以免屡次下载同一图像。
逻辑流程很简单:检查缓存,下载图片,而后在返回以前将图片记录在缓存中。由于咱们是在一个Actor中,这段代码没有低级的数据竞赛;任何数量的图像均可以被同时下载。Actor的同步机制保证每次只有一个任务能够执行访问缓存实例属性的代码,因此缓存不可能被破坏。也就是说,这里的 await 关键字在传达一些很是重要的信息。每当await发生时,就意味着这个函数在此时能够被暂停。
它放弃了本身的CPU,因此程序中的其余代码能够执行,这影响了整个程序的状态。在你的函数恢复的时候,整个程序的状态将发生变化。
重要的是要确保你在等待以前没有对该状态作出假设,而这些假设在等待以后可能不成立。
想象一下,咱们有两个不一样的并发任务,试图在同一时间获取同一图像。第一个任务看到没有缓存条目,开始从服务器上下载图片,而后由于下载须要一段时间而被暂停。
当第一个任务正在下载图像时,一个新的图像可能被部署到服务器上,在同一个URL下。
如今,第二个并发任务试图从该URL下获取图像。
它也没有看到缓存条目,由于第一个下载尚未完成,而后开始第二次下载图像。
当它的下载完成时,它也被暂停。
过了一下子,其中一个下载--让咱们假设是第一个--将完成,它的任务将在Actor上恢复执行。
它填充了缓存,并返回所获得的猫的图像。
如今第二个任务完成了它的下载,因此它被唤醒。
它用它获得的那只悲伤的猫的图像覆盖了缓存中的同一条目。
所以,尽管缓存中已经有了一张图片,但咱们如今在同一个URL上获得了一张不一样的图片。
这就有点让人吃惊了。
咱们指望,一旦咱们缓存了一张图片,咱们老是能在同一个URL上获得相同的图片,这样咱们的用户界面就会保持一致,至少在咱们去手动清除缓存以前是这样。可是在这里,缓存的图片意外地发生了变化。
咱们没有任何低级别的数据竞赛,可是由于咱们在等待中携带了关于状态的假设,咱们最终出现了一个潜在的错误。
这里的修复方法是在等待后检查咱们的假设。
若是当咱们恢复时,缓存中已经有一个条目,咱们就保留原来的版本,丢弃新的。一个更好的解决方案是彻底避免多余的下载。
Actor重入能够防止死锁,并保证向前推动,但它要求你在每一个等待中检查你的假设。
为了更好地设计重入性,在同步代码中执行Actor状态的修改。
最好是在一个同步函数中进行,这样全部的状态变化都被很好地封装起来。
状态的改变可能涉及到将咱们的Actor暂时置于一个不一致的状态。
请确保在等待以前恢复一致性。
记住,await是一个潜在的暂停点。
若是你的代码被暂停,程序和世界会在你的代码被恢复以前继续前进。
你对全局状态、时钟、计时器或你的Actor所作的任何假设,都须要在await以后进行检查。
在本节中,咱们将讨论Actor隔离如何与其余语言特性互动,包括协议符合性、闭包和类。像其余类型同样,只要Actor可以知足协议的要求,它们就能够符合协议。
例如,让咱们让这个LibraryAccount Actor符合Equatable协议。静态的相等方法根据两个图书馆帐户的ID号码进行比较。
由于该方法是静态的,没有自我实例,因此它不是孤立于Actor的。相反,咱们有两个Actor类型的参数,而这个静态方法是在这两个参数以外的。这不要紧,由于这个实现只是在访问Actor的不可变的状态。
让咱们进一步扩展咱们的例子,使咱们的LibraryAccount符合Hashable协议。这样作须要实现**hash(in)**操做,咱们能够像这样作。
然而,Swift编译器会抱怨说这种一致性是不容许的。
发生了什么?嗯,这样符合Hashable意味着这个函数能够从Actor外部调用,可是hash(in)不是异步的,因此没有办法保持Actor的隔离。
为了解决这个问题,咱们能够把这个方法变成非隔离的。
非隔离的意思是,这个方法被视为在Actor以外,即便它在语法上是在Actor上描述的。
这意味着它能够知足Hashable协议的同步要求。
由于非隔离的方法被视为在Actor以外,因此它们不能引用Actor上的可变状态。
这个方法是好的,由于它引用的是不可变的ID号。
若是咱们试图基于其余东西进行哈希操做,好比说借阅的书籍数组,咱们会获得一个错误,由于从外部访问易变的状态会容许数据竞赛。
协议符合性的问题就到此为止。
让咱们来谈一谈闭包。闭包是在一个函数中定义的小函数,而后能够被传递给另外一个函数,在一段时间后被调用。
像函数同样,闭包多是Actor隔离的,也多是非隔离的。
在这个例子中,咱们要从咱们借来的每本书中读出一些,并返回咱们读过的总页数。
对reduce的调用涉及一个执行阅读的闭包。
注意,在对 readSome 的调用中没有 await。
这是由于这个闭包是在与Actor隔离的函数 "read "中造成的,它自己就是与**(actor-isolated)Actor**隔离的。
咱们知道这是安全的,由于reduce操做将同步执行,而且不能将闭包转移到其余线程中,以避免形成并发访问。
如今,让咱们作一点不一样的事情。
我如今没有时间读,因此咱们之后再读。
在这里,咱们建立一个分离式任务。
一个分离的任务在执行闭包的同时,还执行Actor正在进行的其余工做。
所以,这个闭包不能在Actor上,不然咱们会引入数据竞赛。
因此这个闭包并非孤立于Actor的。
当它想调用读取方法时,它必须以异步方式进行,正如 await 所示。
咱们已经谈了一些关于Actor隔离的代码,也就是这些代码是在Actor内部仍是外部运行。
如今,让咱们来谈谈Actor隔离和数据的问题。
在咱们的图书馆帐户的例子中,咱们刻意避免了说书的类型究竟是什么。
我一直假设它是一个值类型,像一个结构。
这是一个很好的选择,由于它意味着图书馆帐户Actor实例的全部状态都是独立的。
若是咱们继续调用这个方法来随机选择一本书来阅读,咱们会获得一个咱们能够阅读的书的副本。
咱们对书的副本所作的改变不会影响到Actor,反之亦然。
然而,若是把这本书变成一个类,事情就有点不一样了。
咱们的图书馆帐户Actor如今引用书的类的实例。
这自己并非一个问题。
然而,当咱们调用这个方法来选择一本随机书时,会发生什么呢?如今咱们有一个对Actor的可变状态的引用,这个引用已经在Actor以外被共享。咱们已经创造了数据竞赛的可能性。
如今,若是咱们去更新书的标题,修改发生在Actor内部可访问的状态中。
由于访问方法不在Actor上,因此这个修改最终可能成为数据竞赛。
值类型和Actor在并发使用时都是安全的,但类仍然会带来问题。
咱们为能够安全地并发使用的类型起了一个名字。Sendable可发送类型。
一个Sendable可发送的类型是一个其值能够在不一样的Actor之间共享的类型。
若是你把一个值从一个地方复制到另外一个地方,而且两个地方均可以安全地修改他们本身的值的副本而不互相干扰,那么这个类型就是可发送的。
值类型是可发送的,由于每一个副本都是独立的,正如在前面谈到的。
Actor类型是可发送的,由于它们同步访问它们的可改变的状态。
类能够是可发送的,但只有在它们被仔细实现的状况下。
例如,若是一个类和它的全部子类只持有不可变的数据,那么它就能够被称为可发送。
或者,若是该类在内部执行同步,例如使用锁,以确保安全的并发访问,那么它就能够是可发送的。
可是大多数类都不是这样的,因此不能被称为可发送类。
函数不必定是可发送的,因此有一种新的函数类型,能够安全地跨Actor传递。
你的Actor--事实上,你全部的并发代码--应该主要以可发送类型进行通讯。可发送类型能够保护代码免受数据竞赛的影响。
这是一个Swift最终会开始静态检查的属性。
到那时,跨越Actor边界传递非可发送类型将成为一个错误。
人们如何知道一个类型是可发送的呢?好吧,Sendable是一个协议,你声明你的类型符合Sendable,就像你对其余协议所作的同样。
而后 Swift 会检查以确保你的类型做为可发送类型是合理的。
若是一个图书结构的全部存储属性都是可发送类型,那么它就能够是可发送的。
比方说,做者其实是一个类,这意味着它--以及做者数组--不是可发送的。
Swift会产生一个编译器错误,代表Book不能是可发送的。
对于泛型类型,它们是不是可发送的,可能取决于它们的泛型参数。
咱们能够在适当的时候使用条件一致性来传播Sendable。
例如,只有当一个对类型的两个泛型参数都是可发送的,它才是可发送的。
一样的方法被用来得出结论,一个可发送类型的数组自己就是可发送的。
咱们鼓励你将Sendable的符合性引入到其值能够安全地并发共享的类型中。
在你的Actor中使用这些类型。
而后,当 Swift 开始执行跨Actor的 Sendable 时,你的代码就准备好了。函数自己能够是可发送的,这意味着跨Actor传递函数值是安全的。
这对闭包尤为重要,由于它限制了闭包能够作的事情,以帮助防止数据竞赛。
例如,一个可发送的闭包不能捕获一个可变的局部变量,由于这将容许局部变量的数据竞赛。
闭包捕获的任何东西都须要是可发送的,以确保闭包不能被用来跨Actor边界移动非可发送类型。
最后,一个同步的Sendable闭包不能被(actor-isolated)Actor隔离,由于这将容许代码从外部运行到Actor上。
在此次演讲中,咱们实际上一直在依赖Sendable闭包的想法。
建立分离任务的操做须要一个Sendable函数,这里写的是函数类型中的**@Sendable**。
还记得咱们在讲座开始时的反例吗?咱们正试图创建一个值类型的计数器。
而后,咱们试图同时从两个不一样的闭包中去修改它。
这将是一个关于可变局部变量的数据竞赛。
然而,因为分离任务的闭包是Sendable,Swift会在这里产生一个错误。
可发送的函数类型被用来指示哪里能够并发执行,从而防止数据竞赛。
下面是咱们以前看到的另外一个例子。
由于分离任务的闭包是可发送的,咱们知道它不该该被隔离到Actor中。
所以,与它的交互必须是异步的。
可发送类型和闭包经过检查易变状态是否在Actor之间共享,以及是否不能被并发修改来帮助维护Actor的隔离。
咱们一直在讨论Actor类型,以及它们如何与协议、闭包和可发送类型交互。
还有一个Actor要讨论 -- 一个特殊的Actor,咱们称之为MainActor。
当你构建一个应用程序时,你须要考虑到主线程。
它是核心用户界面渲染发生的地方,也是处理用户交互事件的地方。
与用户界面有关的操做一般须要在主线程中执行。
然而,你不但愿在主线程上作全部的工做。
若是你在主线程上作了太多的工做,好比说,由于你有一些缓慢的输入/输出操做或与服务器的阻塞交互,你的用户界面会冻结。
所以,你须要注意在主线程与用户界面交互时,在主线程上作工做,但在计算成本高或等待时间长的操做中,要迅速离开主线程。
因此,咱们在能够的时候从主线程上作工做,而后调用DispatchQueue.main.async。
只要你有一个必须在主线程上执行的特定操做,就能够在你的代码中使用async。
从机制的细节中回过头来看,这段代码的结构看起来隐约有些熟悉。
事实上,与主线程的交互就像与一个Actor的交互。
若是你知道你已经在主线程上运行,你能够安全地访问和更新你的用户界面状态。
若是你不在主线程上运行,你须要与它进行异步交互。
这正是演员的工做方式。
有一个特殊的Actor来描述主线程,咱们称之为MainActor。MainActor是一个表明主线程的Actor。
它在两个重要方面与普通的Actor不一样。
首先,MainActor经过主调度队列来执行全部的同步。
这意味着,从运行时的角度来看,MainActor能够与使用DispatchQueue.main互换
第二,须要在主线程上的代码和数据散布在各个地方。
它在SwiftUI、AppKit、UIKit和其余系统框架中。
它分散在你本身的视图、视图控制器和你的数据模型中面向用户界面的部分。
利用Swift并发性,你能够用MainActor属性来标记一个声明,表示它必须在main actor上执行。
咱们在这里对checkedOut操做作了这样的标记,因此它老是在MainActor上运行。
若是你从MainActor以外调用它,你须要等待,这样调用就会在主线程上异步执行。
经过将必须在主线程上运行的代码标记为在MainActor上运行,就不须要再猜想什么时候使用DispatchQueue.main了。
Swift确保这些代码老是在主线程上执行。
类型也能够放在MainActor上,这使得它们全部的成员和子类都在MainActor上。
这对于你的代码库中必须与UI交互的部分颇有用,在这些部分中,大多数东西都须要在主线程上运行。
个别方法能够经过nonisolated关键字选择退出,其规则与你所熟悉的普通Actor相同。
经过对面向用户界面的类型和操做使用MainActor,并引入本身的Actor来管理其余程序状态,你能够构建你的应用程序,以确保安全、正确地使用并发性。
在此次会议中,咱们谈到了Actor如何使用**(Actor isolation)Actor 隔离和要求来自Actor**外部的异步访问来序列化执行,从而保护他们的可变状态免受并发访问。
使用actor在你的Swift代码中创建安全、并发的抽象。
在实现你的Actor和任何异步代码时,要始终为重入性而设计;你的代码中的等待意味着世界能够继续前进,并使你的假设失效。
值类型和Actor一块儿工做,以消除数据竞赛。
要注意那些不处理本身的同步的类,以及其余从新引入共享可变状态的非Sendable类型。
最后,在你与UI交互的代码上使用main actor,以确保必须在主线程上运行的代码老是在主线程上运行。