使包含闭包 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。解决它们的方法是要么阻止访问,使状态不可变,要么提供对状态的有序访问。
我想要一个 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。解决它们的方法是要么阻止访问,使状态不可变,要么提供对状态的有序访问。