概述

演员init / deinit 的行为乍一看似乎很神秘。例如,下面的代码调用了两次print(self.count),但只有第二次给出了警告。

actor Counter {
    var count: Int

    init(count: Int) {
        self.count = count

        print(self.count)
        print(self)
        print(self.count) // ❗️Cannot access property 'count' here in non-isolated initializer; this is an error in Swift 6
    }
}

此警告是为了保护参与者的可变状态免受数据竞争的影响。在本文中,我们将了解演员init / deinit 的行为,包括上面的示例,以便您可以毫无不便地使用它们。

文中的操作验证是使用 Xcode 14 Beta 4 完成的。我们还将 Strict Concurrency Check 设置为 Complete 以警告将来在 Swift 6 中将失败的代码。有关严格并发检查的更多信息,请参阅下面的文章。

TL;博士

  • 在参与者的同步init 中,在执行特定操作(例如将self 作为另一个进程的参数传递)后发生切换到非隔离上下文的衰减
  • 异步init 不会衰减,而是在self 的所有属性都被初始化时隐式挂起并切换到自己的actor上下文
  • 在对actor进行deinit时,除了衰减之外,还有一个限制是即使在衰减发生之前也不能访问不是Sendable的属性

同步初始化衰减

Swift 的init 也可以是异步的,但让我们从同步init 开始。例如,考虑以下演员。

actor Counter {
    var count: Int

    init(count: Int) {
        self.count = count
    }

    func increment() {
        count += 1
    }
}

首先,让我们了解一下为什么会出现我在文章开头提到的警告。您可以通过向init 添加处理来重现它。

actor Counter {
    var count: Int

    init(count: Int) {
        self.count = count

        print(self.count)
        print(self)
        print(self.count) // ❗️Cannot access property 'count' here in non-isolated initializer; this is an error in Swift 6
    }

    // ...
}

仅对第二个print(self.count) 有警告,但如果您在同步init 上对self 执行特定操作,self 将变为非隔离。因为自然。SE-0327根据 ,这种变化称为衰减。
例如,如果self作为函数参数传递,会发生衰减,所以之后self将变为非隔离,无法从同步init访问,因此会发出上述警告。

self的属性像这样在函数中间突然发生变化似乎很奇怪,所以让我们考虑一下为什么需要衰减。
请注意,演员的init 在调用线程中运行,而不是演员的上下文。例如,如果调用UIViewControllerviewDidLoad@MainActor Counter.init,则init 的内容将在主线程中执行。

final class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let counter = Counter(count: 0) // メインスレッドで実行される
    }
}

考虑到同步同步 init 即使您愿意,也没有时间切换上下文,这是很自然的。

考虑到这一点,您可以看到,如果您编写如下代码,可能会发生数据竞争:

func incrementCounter(_ counter: Counter) {
    Task.detached {
        await counter.increment()
    }
}

actor Counter {
    var count: Int
    
    init(count: Int) {
        self.count = count

		// 以下2つの呼び出しでデータ競合が発生し得る
        incrementCounter(self) // 1
        self.increment()       // 2
    }
    
    func increment() {
        count += 1
    }
}

通过1incrementCounter调用的increment从外部调用await,所以它是在actor的上下文中执行的,而increment直接被2调用是在调用线程中执行的。这允许两个线程同时调用increment,而后者又同时写入count,从而导致数据竞争。
这是将self传递给另一个函数引起的问题,init可能会与self并行处理,通过后,如果限制selfinit中直接接触,则不会发生数据竞争.出于这个原因,实现了self,以便在将其传递给另一个函数(即衰减)后它变得非隔离。如果你真的用 Xcode 14 编写了上面的代码,那么衰减会在 2 处发出警告 Actor-isolated instance method 'increment()' can not be referenced from a non-isolated context; this is an error in Swift 6,这样你就会在编译时意识到数据竞争的可能性。

请注意,即使发生衰减,您也可以访问用let 声明的Sendable 属性。这是因为Sendablelet 可以安全地同时被多个线程接触。相反,对于不是Sendable 的属性或定义为var 的属性,即使它们是Sendable,也可能会发生数据竞争,因此在衰减使self 非隔离后访问它们。会给你一个警告.

actor MyActor {
    var nonSendableVar = NonSendableType()
    let nonSendableLet = NonSendableType()
    var sendableVar = SendableType()      
    let sendableLet = SendableType()

    
    init() {
        print(self) // decay によりこれ以降 self は non-isolated
        
        print(nonSendableVar) // ❗️Cannot access property 'nonSendableVar' here in non-isolated initializer; this is an error in Swift 6
        print(nonSendableLet) // ❗️Cannot access property 'nonSendableLet' here in non-isolated initializer; this is an error in Swift 6
        print(sendableVar)    // ❗️Cannot access property 'sendableVar' here in non-isolated initializer; this is an error in Swift 6
        print(sendableLet)    // ✅ OK
    }
}

到目前为止,我们只提到了将self 作为参数传递给另一个函数作为衰减的原因,但实际上衰减也会发生在以下操作中。这是因为数据竞争可以发生而不会衰减,原因类似于将self 作为参数传递给另一个函数。

  • 拨打self方法
  • 访问self 的计算属性
  • self 的属性上触发didSetwillSet 等观察者
  • 闭包捕获self
  • self 存储在内存中

当然,上述行为并不一定会造成数据竞争可能发生的情况。例如,下面的代码不会导致数据竞争,因为它只读取属性的值,但它仍然会衰减并给出警告。

actor Counter {
    var count: Int
    
    init(count: Int) {
        self.count = count
        print(self)
        print(self.count) // ❗️Cannot access property 'count' here in non-isolated initializer; this is an error in Swift 6
    }
}

这是因为编译器要查看self 的使用方式并确定是否会针对每个单独的情况发生数据竞争,而且在某些情况下是不可能的。这是因为它旨在

再次,

  • init 孤立的全球演员
  • nonisolatedinit

但是,如果满足与同步init 相同的条件,则会发生衰减。因为它们都有一个共同点,即它们在参与者的上下文之外运行并出于类似的原因导致数据竞争。

异步初始化中的上下文切换

异步init 的行为与同步init 不同。如果您将使用同步init 发出警告的代码更改为异步init,则警告将消失。

actor Counter {
    var count: Int

    init(count: Int) async {
        self.count = count

        print(self.count)
        print(self)
        print(self.count) // ✅ OK
    }
}

这是因为 async init 在初始化所有属性时隐式切换到参与者上下文。上面的print(self.count)是在actor的上下文中运行的,不像init是同步的,所以同步访问没什么问题。

为了验证上下文切换是否真的发生,让我们从主线程运行多属性参与者init

actor Point {
    var x: Int
    var y: Int
    
    init(x: Int, y: Int) async {
        print(Thread.current.description) // <_NSMainThread: 0x600001704080>{number = 1, name = main}
        self.x = x
        print(Thread.current.description) // <_NSMainThread: 0x600001704080>{number = 1, name = main}
        self.y = y 
        // ここですべてのプロパティが初期化されたのでスレッドが切り替わる

        print(Thread.current.description) // <NSThread: 0x600001700240>{number = 2, name = (null)}
    }
}

xy在初始化之前一直在主线程中运行,但是在初始化之后是在actor的上下文中执行的,所以可以看到线程切换了。这种行为确保increment(由下面的12 调用)不会导致数据竞争,因为它在actor 的上下文中运行。

func incrementCounter(_ counter: Counter) {
    Task.detached {
        await counter.increment()
    }
}

actor Counter {
    var count: Int
    
    init(count: Int) async {
        self.count = count

        incrementCounter(self) // 1
        self.increment()       // 2
    }
    
    func increment() {
        count += 1
    }
}

换句话说,同步init 通过衰减避免数据竞争,而异步init 通过自动切换到参与者上下文来避免它们。这是一种可以采用的方法,因为可以使用异步init 挂起和切换上下文。

为什么上下文切换是隐式的

有趣的是 actor 的异步 init 隐含地做到了这一点,因为上下文切换通常以开发人员可以清楚地看到 await 的方式发生。SE-0327以下是其中的一些原因:

  • 所有属性初始化的时间会因为增加init的参数数量或设置默认参数等变化而改变,所以如果你没有用await等标记上下文切换的时间。如果你做不到,每次都要重写代码很烦人。
  • 首先,在属性初始化期间切换上下文的事实是一个实现细节,开发人员不需要意识到这一点。
  • await被插入到actor的进程中时,需要小心,因为可能会发生actor重入。在这种情况下,不会发生重入,因为除了init之外没有其他人可以访问actor实例,直到属性被初始化。

演员重入,喜欢的可以参考下面的文章。

self 的弱隔离直到属性被初始化

考虑到所有这些,您可以在actor的上下文之外运行时同步设置属性,直到所有属性都被初始化,这似乎很奇怪。

actor Point {
    var x: Int
    var y: Int
    
    init(x: Int, y: Int) async {
        self.x = x // actor のコンテキスト外なのに同期的に値を設定できている
        self.y = y
    }
}

这是因为self 不能被除init 之外的任何人触及,直到所有属性都被初始化,所以self 被有效隔离。SE-0327让我们谈谈这个

依赖于对引用 self 的独占访问的较弱形式的隔离

被表达。事实上,如果你在所有属性初始化之前尝试访问self,你会得到如下错误。

actor Point {
    var x: Int
    var y: Int
    
    init(x: Int, y: Int) async {
        self.x = x
        doSomething(self) // ❗️'self' used in method call 'doSomething' before all stored properties are initialized
                          // self.y が未設定なので init 以外からは `self` を使えない
        self.y = y
    }
}

如果只有init可以碰self,那么自然就不用担心数据竞争了,所以actor的属性可以从上下文之外语法同步设置。相反,如果不允许这样做,则无法从上下文外部初始化 actor 的实例。

同样的原因,您可以同步设置属性,直到使用同步 init 发生衰减。

deinit 中的衰减

到目前为止,我已经为每个同步和异步写了关于 init 的文章,但是对于演员来说,deinit 也有一些需要注意的地方,所以让我们来看看。

deinit 中的衰减

当引用类型的引用计数变为 0 时,从任何线程调用 deinit。这对演员来说也是如此,所以演员deinit 将在演员的上下文之外运行。
脱离上下文意味着可能发生数据竞争,就像 init 与 sync 一样,所以 deinit 也会衰减以避免这种情况。衰减条件与init 相同,在下面的示例中,self 作为参数传递给print 函数,之后self 是非隔离的。

actor Counter {
    var count: Int
    
    deinit {
        print(self.count)
        print(self)
        print(self.count) // ❗️Cannot access property 'count' here in deinitializer; this is an error in Swift 6
    }
}

反之,只有调用deinit的线程才能访问self,直到通过将self作为函数参数传递来满足衰减,所以self被隔离并安全地同步。可以运行。

限制对 deinit 中非 Sendable 属性的访问

除了衰变之外,deinit 还施加了一个限制,即即使在衰变发生之前也不能访问不是Sendable 的属性。

actor MyActor {
    var nonSendableVar = NonSendableType()
    let nonSendableLet = NonSendableType()
    var sendableVar = SendableType()
    let sendableLet = SendableType()
    
    init() {
        print(nonSendableVar) // ✅ OK
        print(nonSendableLet) // ✅ OK
        print(sendableVar)
        print(sendableLet)
        
        print(self)
        
        print(nonSendableVar) // ❗️Cannot access property 'nonSendableVar' here in non-isolated initializer; this is an error in Swift 6
        print(nonSendableLet) // ❗️Cannot access property 'nonSendableLet' here in non-isolated initializer; this is an error in Swift 6
        print(sendableVar)    // ❗️Cannot access property 'sendableVar' here in non-isolated initializer; this is an error in Swift 6
        print(sendableLet)
    }
    
    deinit {
        // init と異なり decay 前の non-Sendable なプロパティへのアクセスもできないようになっている
        print(nonSendableVar) // ❗️Cannot access property 'nonSendableVar' with a non-sendable type 'NonSendableType' from non-isolated deinit; this is an error in Swift 6
        print(nonSendableLet) // ❗️Cannot access property 'nonSendableLet' with a non-sendable type 'NonSendableType' from non-isolated deinit; this is an error in Swift 6
        print(sendableVar)
        print(sendableLet)
        
        print(self)
        
        print(nonSendableVar) // ❗️Cannot access property 'nonSendableVar' here in non-isolated initializer; this is an error in Swift 6
        print(nonSendableLet) // ❗️Cannot access property 'nonSendableLet' here in non-isolated initializer; this is an error in Swift 6
        print(sendableVar)    // ❗️Cannot access property 'sendableVar' here in non-isolated initializer; this is an error in Swift 6
        print(sendableLet)
    }
}

作为一个假设,全局参与者可以安全地在多个实例之间共享非Sendable 属性,因为所有实例都由单个执行程序处理。即使deinit 在actor 的上下文之外执行,该属性也可以在衰减之前同步访问,因此访问该非Sendable 属性的多个线程可能会导致数据竞争。换句话说,一个全局actor可以共享非Sendable属性,假设跟随同一个全局actor的多个实例不会同时处理,但是deinit打破了这一点。

例如,下面的实现可能会导致deinit 上的数据竞争。

class NonSendableCounter {
    var count: Int = 0
    func increment() { count += 1 }
}

@MainActor
final class Container {
    let counter: NonSendableCounter
    
    init() {
        self.counter = NonSendableCounter()
    }
    
    init(sharingCounterWith other: Container) {
        // self と other はいずれも MainActor であるため同時に処理が実行されることはなく、
        // Sendable なプロパティであっても(deinit のことを考えなければ)安全に共有できる
        self.counter = other.counter
    }
    
    deinit {
		// deinit は actor のコンテキスト外で実行されるので self と other の deinit が同時に実行され得る
		// 同時に実行された場合、データ競合が発生する可能性がある
        counter.increment()
    }
}

虽然是人工代码,但通过如下操作上述Container,确认确实发生了数据竞争。

var container1: Container? = .init()
var container2: Container? = .init(sharingCounterWith: container1!)
let container3: Container = .init(sharingCounterWith: container1!)

// counter1 と counter2 の deinit が同時に実行される可能性がある
let t1 = Task.detached { container1 = nil }
let t2 = Task.detached { container2 = nil }

try await (t1.value, t2.value)

print(container3.counter.count) // 1 or 2

我可以想出一种调用Task 的方法来绕过非Sendable 属性不会触及deinit 内部的限制,但最好避免这种情况。

actor MyActor {
    var nonSendableVar = NonSendableType()
    
    deinit {
        Task.detached {
            await print(self.nonSendableVar)
        }
    }
}

deinit在引用计数变为0时被调用,但是如果你像这样在deinit中增加self的引用,引用计数就会增加到1,当它再次变为0时,似乎是未定义的行为并可能导致崩溃。

参考


原创声明:本文系作者授权爱码网发表,未经许可,不得转载;

原文地址:https://www.likecs.com/show-308622787.html

相关文章: