Just want to add that even between your b(1) and b(2) there's a race.
Theoretically speaking, the caller and callee of b() are in different isolation domains, (the caller is @MainActor, the callee is @concurrent nonisolated), so the arguments passed to b, including local which corresponds to the implicit self, must be either Sendable or in a disconnected region. Neither is true for local, so the code should have been rejected.
You can easily verify the existence of this race by extending the operation a little bit longer and turning on TSAN:
public class A {
var a = 0
@concurrent
func b(_ input: Int) async {
for _ in 0..<100 { // <- my modificatons
a = input
}
}
}
@MainActor func doSomething() async {
let local = A()
Task {
await local.b(1)
}
Task {
await local.b(2)
}
try? await Task.sleep(for: .seconds(1))
}
await doSomething()
Console output:
==================
WARNING: ThreadSanitizer: data race (pid=45865)
Write of size 8 at 0x0001043038d0 by thread T1:
#0 ConcurrentAsync.A.a.setter : Swift.Int <null> (ConcurrentAsync:arm64+0x1000022fc)
#1 (1) suspend resume partial function for ConcurrentAsync.A.b(Swift.Int) async -> () <null> (ConcurrentAsync:arm64+0x100002674)
#2 swift::runJobInEstablishedExecutorContext(swift::Job*) <null> (libswift_Concurrency.dylib:arm64e+0x5c450)
Previous write of size 8 at 0x0001043038d0 by thread T2:
#0 ConcurrentAsync.A.a.setter : Swift.Int <null> (ConcurrentAsync:arm64+0x1000022fc)
#1 (1) suspend resume partial function for ConcurrentAsync.A.b(Swift.Int) async -> () <null> (ConcurrentAsync:arm64+0x100002674)
#2 swift::runJobInEstablishedExecutorContext(swift::Job*) <null> (libswift_Concurrency.dylib:arm64e+0x5c450)
Location is heap block of size 24 at 0x0001043038c0 allocated by main thread:
#0 malloc <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x60500)
#1 _malloc_type_malloc_outlined <null> (libsystem_malloc.dylib:arm64e+0x1d9d4)
#2 (1) suspend resume partial function for ConcurrentAsync.doSomething() async -> () <null> (ConcurrentAsync:arm64+0x100001780)
#3 swift::runJobInEstablishedExecutorContext(swift::Job*) <null> (libswift_Concurrency.dylib:arm64e+0x5c450)
#4 start <null> (dyld:arm64e+0xfffffffffff3ab94)
Thread T1 (tid=161502921, running) is a GCD worker thread
Thread T2 (tid=161502922, running) is a GCD worker thread
