Sending NonSendable value error disappear when wrapping the expression in a local closure

In the following code, method testA in MyActor takes in an NonSendable value that it does not own. Here calling function testB with this NonSendable parameter will get an error since we are trying to send this NonSendable value crossing actor boundaries.

class NonSendable {
    var value: Int = 0
    func inc() {
        value += 1
    }
}

@concurrent
func testB(_ value: NonSendable) async {
    value.inc()
}

actor MyActor {
    let value = NonSendable()
    func test() async {
        await testA(a: value)
    }
    func testA(a: NonSendable) async {
        await testB(a)   // Sending 'a' risks causing data races
    }
}

However, if I wrap the expression await testB(a) in a local closure, the error disappears

actor MyActor {
    let value = NonSendable()
    func test() async {
        await testA(a: value)
    }
    func testA(a: NonSendable) async {
        // no error
        let c = {
            await testB(a)
        }
        await c()
    }
}

I’m not very sure yet but it does not look like a desired behaviour. Actually, if we intensionally change the inc function in NonSendable to something like this:

func inc() {
    let newValue = value + 1;
    print("something")
    value = newValue
}

Then run the test method of an instance of MyActor with lots of tasks concurrently:

let a = MyActor()

await withTaskGroup { group in 
    for _ in 0 ..< 100 {
        group.addTask {
            await a.test()
        }
    }
}

We are getting smaller than 100 in the end, which seems like a data race problem.

2 Likes

this is definitely a bug – if you run the example with TSAN it detects the runtime data race. i took the liberty of reporting it here. FYI @hborla @Michael_Gottesman i believe this is a gap in RBI.

1 Like

Thanks! I didn’t know the thread sanitizer in swift before. Looks like a useful tool.

This is probably the same as #79262.

1 Like