Mutex error

I have the code below:

import Synchronization

final class BoxValue {
    let value = 0
}

final class Model: Sendable {
    var value: BoxValue {
        get {
            _value.withLock(\.self)
        }
        set {
            _value.withLock {
                $0 = newValue
            }
            |- error: 'inout sending' parameter '$0' cannot be task-isolated at end of function
            `- note: task-isolated '$0' risks causing races in between task-isolated uses and caller uses since caller assumes value is not actor isolated 
        }
    }

    private let _value = Mutex(BoxValue())
}

I really don't understand the error. For me, it smells like a bug.

Pretty sure it's correct, it just doesn't clearly tell you why. Since BoxValue isn't Sendable, and you pass a new instance in with value.set, technically it's possible you kept another reference to the value passed in and can use it unsafely, despite the passed reference in the Mutex.

4 Likes

Jon's assessment seems correct to me – the Mutex API is designed to try and enforce that you cannot 'sneak' something into it that isn't Sendable and could potentially still be referenced elsewhere, thus introducing potential data races despite protecting mutable state with a lock.

to deal with this constraint, the language offers the sending parameter attribute[1], which indicates that a value is known to be in a 'disconnected' isolation region, so can't be aliased in a different one. currently, however, i don't think there's any way to indicate that property accessors deal in sending values, so i'm not sure the style of convenience accessor you've outlined here can be made to work in this manner (perhaps something like it could be done with macros).

the natural thing to try next to achieve the goal of fully replacing the non-sendable value would be to perform the assignment with a value that is known to be 'disconnected' in this way. i.e. something like:

func assign(_ newValue: sending BoxValue) {
  _value.withLock { $0 = newValue }
}

unfortunately, this formulation currently also fails to compile in the Swift 6 mode, which seems like a bug[2]. the only workarounds i'm aware of involve boxing the sending value within a closure somehow and performing the assignment via that, e.g.

func assign(_ newValue: sending BoxValue) {
  let workaround = { newValue }
  _value.withLock { $0 = workaround() }
}

alternatively, if you pass in a 'producer' closure of sorts, that also seems to work:

func assign(_ newValueFactory: () -> sending BoxValue) {
  // in general this pattern should be avoided
  _value.withLock { $0 = newValueFactory() }
}

however, this latter approach seems quite undesirable since in general one should typically not invoke arbitrary closures while holding a lock.


  1. SE-430 ↩ī¸Ž

  2. a similar issue has been reported here ↩ī¸Ž

2 Likes