详解Swift 中的隔离机制

648 阅读5分钟

引言

随着即将到来的Xcode 16 引入 Swift 6,如果你的工程需要支持Swift 6, 那么当你面对海量的错误提示时,你肯定会非常沮丧。即使你现在没有引入 Swift 6 的紧迫,相信只要这个项目需要迭代,那么你最终会面对这些问题。然而掌握Swift 现代并发模型也并非易事,所以赶紧行动起来吧。

今天我们先来学习 Swift 的隔离机制。隔离是Swift 现代并发模型核心概念之一。

什么是隔离

隔离是 Swift 用来防止数据竞争的机制。它允许编译器推断数据的访问方式,并确保这些访问是安全的。想象一下,隔离就像大海中的一座座孤岛,如果你想要在这些岛屿中交换物资,你必须使用船只,且同一时间只能有一条船可以这个岛屿进行物资交换。

编译时决定隔离

我们可以通过代码定义来查看隔离,这和传统的队列,锁有着本质性的区别。

class MyObject {
    func performTask() {
        // 这是一个非隔离上下文
    }
    
    func asyncPerformTask() async {
        // 即使是异步函数,也仍然是非隔离的
    }
}

然而,查看定义并不总是像听起来那么简单。如果一个类型有超类或符合某些协议,这可能涉及到继承。通常这些定义不在同一个文件,甚至不在同一个模块中,你可能需要查阅它们才能获得完整的隔离信息。

class SubObject: SuperObject, SomeProtocol {
    // 这里的隔离可能取决于继承
    func performTask() {}
}

隔离有三种情况

  1. 无隔离

    class Person {
        // 无隔离
        var name: String = ""
        // 无隔离
        func doSomething() {}
        // 无隔离
        func doSomethingAsync() async {}
    }
    
  2. 静态隔离

    Actor 类型,全局 Actor 都属于静态隔离。其中全局 Actor 中的 @MainActor 最为常见,NSViewController UIViewController 以及 Xcode 16 中 SwiftUI 中 View ,都是使用@MainActor修饰, 运行在主线程。我们在解决 Swift 现代并发模型的编译错误时,使用@MainActor 会给你很大的帮助。

    // 静态隔离
    actor IsolatedPerson {
        // 隔离
        var name: String = ""
        // 隔离
        func doSomething() {}
        // 隔离
        func doSomethingAsync() async {}
    }
    
    // `updateUI` 运行在主线程
    @MainActor func updateUI() {}
    
  3. 动态隔离

    assumeIsolated 在一种动态隔离,它用于向编译器承诺当前的代码执行上下文已经在某个特定的隔离域(例如 MainActor)中。这通常是在编译器无法静态推断出代码确实在该隔离域中执行时使用的。

    @MainActor
    class MyMainActorClass: SomeDelegate {
        nonisolated func someDelegateCallback() {
            MainActor.assumeIsolated {
                // 在此处访问 MainActor 上的内容
                self.updateUI()
            }
        }
        func updateUI() {}
    }
    

Task继承隔离

使用Tak.init()可以继承当前的隔离, 而使用 Task.detached()无隔离

@MainActor
class IsolatedClass {
    // 隔离
    func doSomething() {
        // 继承隔离,仍然隔离在 MainActor 上!
        Task {}
        // 无隔离
        Task.detached {}
    }
}

nonisolated退出隔离

如果某些东西具有你不想要的隔离,你可以使用 nonisolated 关键字选择退出。这对静态常量(不可变且安全访问的)尤其有意义。

actor IsolatedClass {
  	//无隔离方法
    nonisolated func nonIsolatedMethod() {}
    nonisolated static let nonIsolatedString = "Tesla"
}

隔离与协议

协议定义了符合类型必须满足的要求,包括静态隔离。这可能导致协议声明与符合类型之间的隔离不匹配。

//无隔离
protocol NonIsolatedProtocol {
    func performTask()
}

//全部隔离在 main actor 上
@MainActor
protocol MainActorIsolatedProtocol {
    func performTask()
}

protocol PartiallyIsolatedProtocol {
  	// 部分隔离在 main actor 上
    @MainActor
    func performMainTask()
}

跨隔离域访问

指的是在不同隔离域之间访问数据或执行操作。隔离域是 Swift 的并发模型中用来确保线程安全的一种机制。每个隔离域确保某些代码在执行时不会与其他隔离域的代码发生数据竞争

// 使用 @MainActor 标记的类,其所有方法都将在主线程上执行
@MainActor
class MainActorClass {
    var count: Int = 0
    
    func increment() {
        count += 1
        print("Count incremented to \(count) on the main actor.")
    }
    
    func asyncIncrement() async {
        await Task.sleep(1 * 1_000_000_000) // 模拟异步任务
        count += 1
        print("Async count incremented to \(count) on the main actor.")
    }
}

// 一个非隔离的全局函数
func nonIsolatedFunction() {
    print("This function is running outside of the MainActor.")
}

// 一个非隔离的类
class NonIsolatedClass {
    func performTask() async {
        // 在非隔离的上下文中调用隔离的方法
        print("Calling MainActor method from non-isolated context...")
        let mainActorInstance = MainActorClass()
        await mainActorInstance.asyncIncrement()
        // 在非隔离上下文中调用非隔离的全局函数
        nonIsolatedFunction()
    }
}
结语

通过这篇文章,我们深入探讨了 Swift 的隔离机制,这一机制在 Swift 现代并发模型中占据了核心地位。无论是静态隔离、动态隔离,还是跨隔离域的访问,理解并掌握这些概念对于编写安全且高效的并发代码至关重要。

隔离不仅帮助我们防止数据竞争,还简化了并发编程的复杂性,使得代码更加可预测和可靠。虽然 Swift 6 带来了许多新的特性和挑战,但正是这些挑战推动我们不断进步和学习。

如果你能在自己的项目中熟练应用这些隔离机制,那么你将能够更从容地应对 Swift 6 所带来的变革,并为未来的开发奠定坚实的基础。希望这篇文章能帮助你更好地理解和运用 Swift 的隔离机制,也期待在未来的项目中,你能写出更加安全、健壮的代码。