在Mutex与属性包装器的用法中,如何支持不可发送的值

移动开发 2026-07-09

我有这样的代码:

@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不支持sendingnewValue参数。

为了让互斥锁在处理不可发送的类型时安全工作,它需要确保被保护的值不会被隔离到其他地方。例如,下面的写法就是不安全的,也不会按预期编译通过。

// 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导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章