Strict concurrency checking in Swift 5.x and 6.0
Swift 6 aims to detect many concurrency issues at compile time. Fixing these new warnings and errors at compile time should prevent so-called “data-races” from showing up as intermittent run-time errors. This goal is important and even urgent because software developers are increasingly relying on concurrency to utilise multi-core processors and to perform slow tasks like network requests on background threads.
Expressed differently, the Swift 6 compiler forces you (after optin) to follow certain rules that should guarantee that data-races between threads cannot occur. This “cannot occur” involves one or more of these rules:
- ensuring that the shared data cannot change (“immutability”, e.g. via
let
) - ensuring that the passed data conforms to the
sendable
protocol (e.g. value types) - ensuring that the code is isolated to one actor (concurrency context) and thus can never be used elsewhere
- providing some soft of serialisation, thus ensuring that a thread is finished with a critical update before another thread can start with the same code
give problems. This “provability” approach can imply that you end up fixing cases that theoretically could happen, but won’t because the compiler is not sure that they cannot occur. But the Swift.org community is working hard at minimising the number of false alarms.
Migration path and ‘optin’ settings
The Swift standard has been working towards this goal for a few years. In recent releases of Swift 5.x new capabilities were added stepwise – aiming at full data-race safety being guaranteed with the release of Swift 6.0 (Summer 2024).
Alternatives for fixing errors/warnings detected by Swift 5.10’s strict or complete concurrency checking (-strict-concurrency=complete
):
@MainActor
@MainActor
ensures a class or method is only accessible from the main actor (“isolated to a global actor” – doesn’t have to be Main but usually is).
This ensures that the class is only accessible to one thread.
It essentially blocks concurrent access, to achieve data-race safety.
Sendable
Ensures that the type conforms to Sendable
, and is marked final
.
The Sendable
protocol declares that the class only contain immutable (value type) properties.
Notably let properties are immutable, but so are value types like structs and Int
and String
. A type may implicitly conform to Sendable
if all its properties are Sendable
.
final
blocks creation of subclasses which could contain new mutable properties.
With Sendable
the compiler will check whether the type is indeed provably Sendable
.
But this checking does not cross file boundaries (?).
Sendable
checking is a bit strict (and may become a bit more flexible in Swift 6 to reduce the number of false positives).
The Sendable
feature was added in Swift 5.5 (September 20, 2021).
@unchecked Sendable
This is applied to a type.
This option essentially just disables the compile-time warnings.
It disables checking for all properties in the type, so is a rather blunt instrument.
@Sendable
Used to make functions sendable
: you cannot make a function conform to a protocol like Sendable
.
nonisolated(unsafe)
Declare a property as nonisolated(unsafe)
if you know there can never be a concurrency problem SE-412. Available in Swift 5.10 (March 5, 2024).
This option essentially just disables the warnings.
It can be used on stored properties, local variables, and global/static variables.
@preconcurrency
This turns off checking in an imported module.
And is meant if you don’t have time yet to fix it. Or don’t have access to the 3rd party source code.
Although the compiler will actually tell you if usage of @preconcurrency
wasn’t necessary, this fix should be used sparingly.
References
https://www.swift.org/blog/swift-5.10-released/
https://www.avanderlee.com/swift/concurrency-safe-global-variables-to-prevent-data-races/
https://www.avanderlee.com/swift/race-condition-vs-data-race/
https://www.massicotte.org
https://code.kiwi.com/articles/swift-5-10-concurrency-survival-guide/