0

我想要一个Sendable包含闭包的结构。此闭包接受引用类型,但返回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定义上,我收到以下警告:

符合“Sendable”的结构“Greeter”的存储属性“greet”具有不可发送类型“(Person)-> Void”

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

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

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

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

4

1 回答 1

1

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

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

一个简单的例子:

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 中不断增加的警告级别和缺乏指导,我试图弄清楚这些东西。‍♂️</p>


让它成为一个演员:

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在包装器之外存储和变异的代码。

创建/复制的东西:

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

所有并发问题的根源是可变共享状态。解决它们的方法是阻止访问,使状态不可变,或者提供对状态的有序访问。

于 2022-02-02T15:30:25.703 回答