Exploring @unchecked Sendable in Swift Concurrency
iOS swift Concurrency Sendable Estimated reading time: 3 minutesSwift’s Sendable
protocol is a cornerstone of the language’s concurrency model, marking types that can be safely shared across concurrent contexts. When the Swift team introduced structured concurrency in Swift 5.5, they needed a way to ensure thread safety at compile time.
@unchecked Sendable
serves as an escape hatch for types that can’t automatically satisfy Sendable
requirements but are known to be thread-safe through other means. It tells the compiler “I know what I’m doing” when you can’t prove thread safety through Swift’s normal mechanisms.
Historical Context and Proposal
The concept of Sendable
and its unchecked variant emerged from SE-0302: Sendable
and @Sendable
closures, which was part of Swift’s larger concurrency story.
Key points from the proposal:
- Originally called
ConcurrentValue
, renamed toSendable
before final implementation - Designed to prevent data races by marking thread-safe types
@unchecked Sendable
was included for legacy code and special cases (previouslyUnsafeSendable
)
The Swift
team recognized that some thread-safe types couldn’t mechanically satisfy Sendable
requirements because:
- They used internal synchronization
- They were inherently immutable despite containing
non-Sendable
components - They wrapped
non-Sendable
system types that were known to be safe
Example
A simple example is next:
final class Counter: @unchecked Sendable {
private var value: Int = 0
private let queue = DispatchQueue(label: "com.counter.syncQ")
func change(_ newValue: Int) {
queue.sync {
self.value = newValue
}
}
func retrive() -> Int {
queue.sync { value }
}
}
Another alternative for isolation may be region-based concurrency isolation SE-414:
Task { @MainActor in // some code }
Practical Use Cases
The scope for using this attribute is rather extensive, which means we have a significant amount of legacy code or code that we are certain is thread-safe, yet it cannot be adapted to conform to Sendable
:
- Wrapping
Non-Sendable
Types (because of manual synchronization that provides better performance or due to other reasons)
A good example here is how Vapor
handle their Model
with new concurrency system
import Vapor
struct UserModule: ModuleInterface, @unchecked Sendable {
func boot(_ app: Application) throws {
// do stuff
}
}
- Legacy Code Integration (for example when legacy/system integration requires flexibility)
final class EmailContact: @unchecked Sendable {
private var _emails: Array<Email> = []
private let lock = NSLock()
var emails: Array<Email> {
lock.withLock { _emails }
}
@discardableResult
func remove(at index: Int) -> Email {
lock.withLock { _emails(at: index) }
}
func add(_ emails: PhoneNumber) {
lock.withLock { _emails.append(emails) }
}
}
- Low-Level System Wrappers
extension ArraySlice: @unchecked Sendable
where Element: Sendable { }
- Actor State Snapshots
- Testing Concurrency
- and many other
Despite existing of this danger (as for me) attribute, u must always remember a few things that can improve it’s usage:
- Document thoroughly why a type is
@unchecked Sendable
- Prefer proper
Sendable
conformance when possible - Write concurrent tests to verify thread safety
- Consider using
actors
as alternatives
Remember (as mentioned in official doc):
/// To declare conformance to
Sendable
without any compiler enforcement
/// write@unchecked Sendable
/// You are responsible for the correctness of unchecked sendable types,
/// for example, by protecting all access to its state with a lock or a queue.
We may compare 2 main option as follow:
Operation | Runtime Cost | Thread Safety Mechanism | Use Case |
---|---|---|---|
Actor Sendable |
Low (actor hop) | Full actor isolation | Safe reference to actor state |
@unchecked Sendable |
Medium (queue sync) | Manual synchronization | Legacy/Custom synchronization |
Conclusion
Swift if still in active development, so we may discover something new at every corner.
Swift’s @unchecked Sendable
and @returnsIsolated
form a powerful duo for bridging Swift’s strict concurrency model with real-world programming needs.
Remember: concurrency tools are like surgical instruments—powerful when used precisely, dangerous when wielded carelessly. Choose the right tool for each task, and always know why you’re reaching for the “unchecked” option.
Resources
Share on: