本期WWDC21的演讲来自于苹果公司Swift团队的工程师,Dario Rexin。Dario会为我们介绍Swift Actor模型,并介绍Actor是如何保护Swift并发应用中的可变状态(Mutable State)的。
在写并行程序是最重要的一个问题之一是如何避免资源竞争。当有两个线程同时请求同一个数据时,且其中至少一个是写操作。资源竞争的bug是非常棘手的。资源竞争是被共享的可变状态所导致的。一种避免资源竞争的方式是使用值语义,对于一个特定类型的变量,所有变化都是本地的,此外,还可以令值语义类型成为真正的不可变化的,这样通过不同的进程就可以安全的访问他们。
Swift自始就一直在倡导值语义,因为这可以更便于使用并行进程。Dario介绍了一个值语义保护避免资源竞争的例子。
Swift标准库中的主要类型都有值语义。Dario进一步举了一个使用结构体中的例子,
在counter声明为“let”时,编译器会无法编译,因为counter类型的increment函数不允许发生变化,而当counter声明为“var”时,编译器也会不允许有并行任务同时改变他而编译不通过,而当两个线程各自用一个局部变量赋值并进行改变后,资源竞争的问题就解决了,然而并没有达到代码想达到的效果。此时就需要共享可变状态。并行程序中的共享可变状态需要同步来保证避免资源竞争。现在存在的许多的线程同步工具,如Atomics, Locks, Serial dispatch queues等,但他们都有同一个缺点:需要谨慎调用来保证其正确性。因此,Actor就有了独特作用。
Actor会为共享可变状态提供同步,并有独自的、与程序中剩余部分都分割的状态,且只有通过Actor才能控制到那个状态,且每次仅会有一个Actor能够控制该状态。Actor是Swift中一个新的类型,它和其他的类型、结构体等十分相似。Actor类最独特的一点在于,他们会把他们的实例与程序的剩余部分区分开,并保证对数据的同步。
Dario举了一个Actor的使用例子,在对Actor类进行操作时,其会自己保护不会有其他进程同时进行操作,来防止资源竞争的问题。当有多个Actor企图对同一个资源进行操作时,Swift有一个机制,会令后来的线程进入等待,在等待时CPU可以继续完成其他任务,在之前的Actor使用资源结束后,会自动继续完成另一个Actor的线程,来保证Actor的函数来进行时不会被其他线程打断。
Dario接下来介绍了Actor reentrancy。即当Actor进入await状态,有其他Actor进行操作时,Actor reentrancy可以防止死锁,并保证后续的运行内容,但是需要用户自己考虑在await状态时,可能发生的情况并进行排除和避免。
接下来,Dario的同事Doug进一步介绍了Actor的独立性是如何与其他语言特性交互的。Doug举了Actor结合判断相等和结合哈希的例子,说明了Actor在结合其他功能时,在保证功能本身以外,还需要保证Actor自身良好的内外调用的分离,即在定义函数时许考虑清楚函数本身是否在Actor内被调用,若在Actor外被调用,则需定义为unisolated,来让其被当做Actor外的函数,来保护Actor类内变量。此外,在Actor内外传送数据时,也需要注意不是所有类型的数据都是可以安全地同时读写的,对于那些可以安全同时操作的数据,叫作Sendable可传送的。在Swift中,可以为类加一个一致性,那么Actor就会去检查这个类是否被封装好可以成为一个Sendable的类,函数同样也有一些成为Sendable的限制,且也会被Actor进行检查是否可以通过。
最后Dario还介绍了一种最特殊的Actor类型:Main Actor,即用来表达程序中主线程的Actor。Main actor与普通的Actor最大的区别在于两点:1.Main actor在他的主调度队列实现所有的同步工作,主线程中,散落在程序各地的代码内容,都可以同步仅在Main Actor里进行工作。