在Mutex与属性包装器的用法中,如何支持不可发送的值
我有这样的代码:
@available(iOS 18.0, *)
@propertyWrapper
public final class Atomic<Value: Sendable>: Sendable {
private let mutex: Mutex<Value>
public init(wrappedValue: Value) {
self.mutex = Mutex(wrappedValue)
}
public var wrappedValue: Value {
get { mutex.withLock { $0 } }
set { mutex.withLock { $0 = newValue }}
}
public var projectedValue: Atomic<Value> { self }
}
在我的游戏中的用法:
@Atomic
private(set) var cameraTransform: Transform = .identity
现在我需要保护一个不可发送的对象。我发现如果直接使用Mutex(不带属性包装器),它确实能工作,因为Mutex本身并不需要Sendable,它只需要sending个参数和返回值。
但看起来属性包装器似乎不允许我使用可发送的参数和返回类型。我想知道是否有变通方法,既能支持不可发送的类型,又能通过属性包装器节省一些代码?
解决方案
目前确实存在一些编译器错误(示例),会产生假阳性和假阴性,这让事情更加困惑,但归根到底,属性包装器并不能实现,因为setter不支持sending的newValue参数。
为了让互斥锁在处理不可发送的类型时安全工作,它需要确保被保护的值不会被隔离到其他地方。例如,下面的写法就是不安全的,也不会按预期编译通过。
// suppose this is declared in 'Atomic' and
// 'Value' is a type parameter without constraints
func set(_ newValue: Value) {
mutex.withLock { $0 = newValue }
}
请考虑这种不安全的情形:
actor SomewhereElse {
var atomic = Atomic<NonSendable?>(wrappedValue: nil)
func doSomeIsolatedStuff() {
print(atomic.wrappedValue)
}
}
@MainActor
class Foo {
var x = NonSendable()
func f() async {
let somewhereElse = SomewhereElse()
await somewhereElse.atomic.set(x)
// SomewhereElse can now access x through the mutex
// but the MainActor can also access it, without the mutex!
}
}
为了让set安全,newValue参数需要是sending,这意味着调用方承诺不再访问该参数。
func set(_ newValue: sending Value) {
mutex.withLock { $0 = newValue }
}
属性setters的newValue参数不能是sending,因此不可能实现一个既安全又能包装一个Mutex、同时适用于不可发送值的属性包装器。
答案的其余部分在讨论编译器错误,这实际上让创建这样的属性包装器成为可能。不过如前所述,从并发角度来看,这将是不可安全的。
存在一个编译器错误,使得带有sending参数的上述set声明仍然无法编译!幸运地找到了一个变通方法——把参数包裹在一个闭包中可以让编译器满意,
// this compiles!
func set(_ newValue: sending Value) {
let block = { newValue }
mutex.withLock { $0 = block() }
}
但如果我移除sending,实际上仍然会编译通过(错误报告)!
// this compiles and is unsafe!
func set(_ newValue: Value) {
let block = { newValue }
mutex.withLock { $0 = block() }
}
利用这个假阳性,你可以写成
@available(iOS 18.0, *)
@propertyWrapper
public struct Atomic<Value>: Sendable, ~Copyable {
private let mutex: Mutex<Value>
public init(wrappedValue: @autoclosure () -> sending Value) {
self.mutex = Mutex(wrappedValue())
}
public var wrappedValue: Value {
get { mutex.withLock { $0 } }
set {
let workAround = { newValue }
mutex.withLock {
$0 = workAround()
}
}
}
}
请注意,init需要一个@autoclosure () -> sending Value,而不是你所预期的一个简单的sending Value。这是因为又一个编译器错误。
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。