使包含闭包 Sendable 的结构安全吗?

Is it safe to make a struct containing a closure Sendable?

我想要一个 Sendable 结构,它包含一个闭包。此闭包采用引用类型,但 returns Void,因此 Greeter 不直接存储 Person 引用。但是,无论如何,闭包本身仍然是引用。

当前代码:

class Person {
    let name: String

    init(name: String) {
        self.name = name
    }
}

struct Greeter: Sendable { // <- Trying to make this Sendable
    let greet: (Person) -> Void

    init(greeting: String) {
        greet = { person in
            print("\(greeting), \(person.name)!")
        }
    }
}
let person = Person(name: "George")
let greeter = Greeter(greeting: "Hello")
greeter.greet(person)

// Hello, George!

在我的实际问题中(这是简化的)我实际上并不知道 Person 的实现,因此无法标记它 Sendable。它实际上是一个 MTLRenderCommandEncoder,但为了简单起见,我们只有 Person.

greet 定义中,我收到以下警告:

Stored property 'greet' of 'Sendable'-conforming struct 'Greeter' has non-sendable type '(Person) -> Void'

我可以让警告消失,但我认为这不是安全和正确的解决方案:

struct Greeter: Sendable {
    let greet: @Sendable (Person) -> Void

    init(greeting: String) {
        greet = { @Sendable person in
            print("\(greeting), \(person.name)!")
        }
    }
}

如何确保此代码跨线程安全?

我认为你不能。其他人可能有对 Person 的引用,同时修改它并打破你的假设。

但是您可以创建一个 PersonWrapper: @unchecked Sendable,如果有多个引用或将其存储为序列化的 Sendable 类型,则可以复制 Person。这可能会很昂贵,但会很安全。如果您进行更改,您可能还必须锁定,并且 return 复制而不是真实的东西。

一个简单的例子:

public struct SendableURL: Sendable {
    private let absoluteString: String
    public init(_ url: URL) {
        self.absoluteString = url.absoluteString
    }
    public var url: URL {
        URL(string: absoluteString)! 
    }
}

处理不可序列化对象的版本是:

public final class SendablePerson: @unchecked Sendable {
    private let _person: Person
    private init(_ person: Person) {
        self._person = person
    }
    public static func create(_ person: inout Person) -> SendablePerson? {
        let person = isKnownUniquelyReferenced(&person) ? person : person.copy()
        return SendablePerson(person)
    }
    public func personCopy() -> Person {
        _person.copy()
    }
}

你怎么看?我认为只要避免共享可变状态就应该没问题。如果您无法复制您依赖的对象未被修改。

实际上,我们每天都会通过线程做不安全的事情(例如传递Data/UIImage等)。唯一的区别是 SC 在所有情况下都更严格地避免数据竞争,并让编译器推理并发性。

面对 Xcode 中不断增加的警告级别和缺乏指导,我正试图弄清楚这些东西。 ‍♂️


让它成为演员:

public final actor SendablePerson: @unchecked Sendable {
    // ...
    public func add(_ things: [Something]) -> Person {
        _person.array.append(contentsOf: things)
        return _person
    }
}

或使用 lock()/unlock() 启动每个实例方法。

public final class SendablePerson: @unchecked Sendable {
    // ...
    private let lock = NSLock()

    public func add(_ things: [Something]) {
        lock.lock()
        defer { lock.unlock() }
        _person.array.append(contentsOf: things)
        return _person
    }

    // or 
    public func withPerson(_ action: (Person)->Void) {
        lock.lock()
        defer { lock.unlock() }
        action(_person)
    }
}

在这两种情况下,每个方法都将在调用另一个方法之前完全执行。如果一个锁定的方法调用另一个锁定的方法,用 NSRecursiveLock 替换 NSLock。

如果您无法提交 Person 份副本,请注意不要将引用传递给在包装器外部存储和改变 Person 的代码。

create/copy 事情:

  • 如果一个线程正在同时改变 Person 的状态,我不能保证我从 Person 读取的内容在我对它采取行动之前仍然是真实的。但是如果我手动复制,我知道线程最多会修改自己的副本。
  • 创建是一种创建包装器以尝试同步更改的方法。

所有并发问题的根源是mutable shared state。解决它们的方法是要么阻止访问,使状态不可变,要么提供对状态的有序访问。