Why do I need explicit @Sendable to capture a Mutex in a sending closure?

first i want to highlight some recent similar topics that may be of interest:

  1. Sending, inout sending, Mutex
  2. Sending to TaskGroup child task

now, on to my speculations as to what's going on here...

note that this behavior is not limited to Mutex; you can end up with the same diagnostic from this reduced example with a subset of the Mutex API:

struct NC: ~Copyable {}
extension NC: @unchecked Sendable {}

func f() async {
    let nc = NC()
    await withTaskGroup(of: Void.self) { taskGroup in
        taskGroup.addTask {
                          `- error: passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure
            _ = nc
                `- note: closure captures reference to mutable let 'nc' which is accessible to code in the current task
        }        
    }
}

now, removing the ~Copyable suppression syntax resolves the issue[1], so presumably the diagnostic has something to do with the type being non-copyable.

consider a similar configuration, where we have a mutable capture of a Sendable type, e.g.

func f() async {
    var i = 0
    await withTaskGroup(of: Void.self) { taskGroup in
        taskGroup.addTask {
                          `- error: passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure
            _ = i
                `- note: closure captures reference to mutable var 'i' which is accessible to code in the current task
        }
    }
}

the diagnostics are essentially identical, which suggests the region-based isolation checking is perhaps treating these two cases as equivalent. i can imagine why this might happen; a mutable capture of a Sendable type is sort of like wrapping the sendable entity in a (non-sendable) 'box' of sorts. similarly, a non-copyable type can perhaps be thought of as a value contained in a 'mutable box', with some additional requirements about how things can move in and out of it.

it seems like this is probably a bug – a Sendable type (and in particular Mutex) should be able to be passed across these isolation boundaries without issue.


  1. as does the addition of an explicit @Sendable annotation on the closure, as you illustrate in your example. ↩︎