I wrote this for a little CLI tool I'm making. It's only important that it can receive & run its function calls immediately, because it's for logging, where placing the lines in-order is important.
import Foundation
fileprivate final class OutputCoordinator {
private var output = String()
private init() {}
deinit {
flushOutput()
}
func prepare(outputLine line: String) {
output += line
output += "\n"
}
func flushOutput() {
defer { output = .init() }
Swift.print(output)
}
static let shared = OutputCoordinator()
}
func print(queueing items: Any...) {
let line = items.map(String.init(describing:)).joined(separator: " ")
OutputCoordinator.shared.prepare(outputLine: line)
}
I work with warnings-as-errors, so it fails with this:
Static property 'shared' is not concurrency-safe because it is not either conforming to 'Sendable' or isolated to a global actor; this is an error in Swift 6
The only way I could figure out to get this to compile is with @unchecked Sendable
:
fileprivate final class OutputCoordinator: @unchecked Sendable {
Surely this isn't the intended way the Swift language designers want me to do this. What should I be doing instead?
The compiler is warning you that your type is not thread-safe. To indicate that it was thread-safe, we would make it Sendable
. That means you would either:
Make it an actor:
actor OutputCoordinator {…}
Isolate it to some global actor, such as the main actor:
@MainActor
final class OutputCoordinator {…}
You could also create your own global actor for this, too. But, just making the type an actor (point 1, above) would be simpler.
Or manually make it Sendable
:
final class OutputCoordinator: @unchecked Sendable {
static let shared = OutputCoordinator()
private var output = ""
private let queue = DispatchQueue(label: "output.coordinator")
deinit {
flushOutput()
}
func prepare(outputLine line: String) {
queue.async { [self] in
output += line
output += "\n"
}
}
func flushOutput() {
queue.async { [self] in
print(output)
output = ""
}
}
}
Note, I generally would use locks for manual synchronization, but you suggested that FIFO behavior was important, so I have used a queue here. But given that you said that this is a single-threaded app, that’s not a concern, so I would probably do:
final class OutputCoordinator: @unchecked Sendable {
static let shared = OutputCoordinator()
private var output = ""
private let lock = NSLock()
deinit {
flushOutput()
}
func prepare(outputLine line: String) {
lock.withLock {
output += line
output += "\n"
}
}
func flushOutput() {
lock.withLock {
print(output)
output = ""
}
}
}
In general, when merely trying to eliminate data races, I would lean towards an actor. But given that you need strict FIFO behaviors (which actors do not guarantee), you might want to use the dispatch queue pattern.
Regardless, as I think you already guessed, the key is that you would never use @unchecked Sendable
without actually synchronizing the internal state. The compiler is warning you that your original code snippet was not thread-safe, and you should never declare @unchecked Sendable
conformance without actually making it thread-safe.