Possibly a naive question (or a duplicate), so apologies but I can't get my head around this:
class C {
@MyActor func somePublicFunction() -> Int { foo } // this works
@MyActor private var foo: Int = 0
private func bar() {
Task.detached { @MyActor in
self.foo = 1 // Warning: "Capture of self <...>"
}
}
}
Swift 5.10, concurrency checks are on (Complete).
Here is what I can and can not do, and this is mostly due to the nature of the code I'm working on, it uses CoreAudio but tries to be entirely Swift concurrency-friendly on the interface level:
I can't touch class C, it shouldn't be bound to any actor or become an actor.
Neither can class C be converted to struct.
The context of the bar() call is a high-priority system thread (audio rendering) where not much can be done, i.e. it's a quick fire-and-forget context, hence the use of Task.detached.
I know that variable foo will only be accessed via MyActor, so no harm can possibly be done by the above code.
So how do I silence the compiler? Or is it indicating I'm actually doing something wrong?
The compiler is warning you that it's not safe to access the C instance concurrently, because it is not Sendable. Even though your access of self is safe due to being restricted to a subset of self that happens to be appropriately isolated, by capturing self in that closure you're now able to access anything on self which is unsafe.
Solutions I can think of:
• Conform C to Sendable. I think that will "just work" because all the state is actor isolated, but I might be forgetting a rule that spoils that. However that's a public interface change and based on your requirements this may not be acceptable (it counts as "messing with class C). If you get warnings or errors from adding this conformance, you can change the conformance to @unchecked Sendable which just tells the compiler to be quiet. I would treat this as a last ditch effort though because it's opting out of compile enforced concurrency safety.
• Extract foo into a private class inside C that conforms to Sendable and has the MyActor isolation applied to the whole class. Hold an instance (as a let, like let fooWrapper: FooWrapper) of this class inside C instead of foo directly, and in the detached Task block capture the instance explicitly (add [fooWrapper] after the block opening brace, before or after the actor attribute, can't remember which one is correct). And of course don't access it through self in the block. What this does is tell the compiler you are only capturing that single piece, not the entire C instance, and that is guaranteed to be safe. You can also keep foo as a computed var for convience. Note this inner type must be a class because it requires reference semantics so you can capture the inner instance only and still mutate it. It can't be a value type like struct. Under the hood you are moving foo out of the C instance into its own separate object which allows you to capture only that piece in blocks without capturing all of the C instance.
Thanks, I forgot about @unchecked Sendable! I put in the root of my hierarchy and it silenced all concurrency warnings everywhere, but now it's a minefield as the compiler can't give guarantees of correctness.
I was hoping to get this working with solid concurrency guarantees, so I'm going to review the architecture and likely go back to semaphores and tell the users of my framework "call anything from anywhere". The high priority system thread shouldn't be messed with and it seems like bridging the two worlds will be challenging one way or the other.
I think that's nice solution, which keeps compiler checks, and also isolations separate.
Works pretty well
@globalActor
actor MyActor: GlobalActor {
static let shared = MyActor()
}
final class C: Sendable {
private let sideState = SideState()
private func bar() {
Task.detached { @MyActor in
self.sideState.foo = 1
}
}
@MyActor
private final class SideState {
func somePublicFunction() -> Int { foo }
var foo: Int = 0
}
}
I wish it was that easy but class C can't be made Sendable due to the functionality that's there. If you read the above comments, it's been suggested already to have @unchecked Sendable which silences the compiler but shifts the responsibility onto me.
Because you're still capturing self. Instead capture sideState:
Task.detached { @MyActor [sideState] in
sideState.foo = 1
}
You don't need to mark C as Sendable. You might not even need to mark SideState as Sendable, although it intrinsically is due to being actor isolated so it makes sense to mark it.
Working with low level and especially real time systems like audio it can definitely make sense to use semaphores, or mutexes, to protect state, if the performance characteristics of switching threads to enter critical sections isn't acceptable. The full Swift concurrency solution using actors and async calls/tasks, which involves switching threads, might not play well with the timing requirements.
But you can still get help from the compiler instead of just marking the whole sub-system/facade as @unchecked Sendable and losing all protections.
Instead, make a type that encapsulates protected state and handles the access protection and mark that as @unchecked Sendable, then just carefully test that abstraction. Then the compiler will still remind you to protect any concurrently accessed state, which you can do by wrapping it in this type.
If you want to use semaphores:
public final class Protected<T>: @unchecked Sendable {
public init(
value: T
) {
_value = value
}
public var value: T {
_read {
semaphore.wait()
defer { semaphore.signal() }
yield _value
}
_modify {
semaphore.wait()
defer { semaphore.signal() }
yield &_value
}
}
public func read<R>(
work: (T) throws -> R
) rethrows -> R {
semaphore.wait()
defer { semaphore.signal() }
return try work(_value)
}
public func write<R>(
work: (inout T) throws -> R
) rethrows -> R {
semaphore.wait()
defer { semaphore.signal() }
return try work(&_value)
}
private var _value: T
private let semaphore = DispatchSemaphore(value: 1)
}
(Side note: using semaphores like this to enforce single thread access isn't ideal because it doesn't handle priority inversion properly, it would be better to use locks, Darwin even has a read write lock so you can allow concurrent reads).
You can build similar abstractions for protecting state with other means like dispatch queues. By concentrating the @unchecked Sendable around these wrappers that use protection methods the Swift compiler doesn't understand, you can essentially "teach" the compiler about them (it has to trust your implementation though) and then it can incorporate them into its concurrency protection rules.
You can make Protected a property wrapper but since that requires using var, which applies to the wrapped value and the wrapper, it ends up not helping you with handling concurrency warnings (the wrapper being mutable is unsafe and will trigger warnings).