Mutation of captured var in concurrently-executing code - swift

I had an issue in Swift 5.5 and I don't really understand the solution.
import Foundation
func testAsync() async {
var animal = "Dog"
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
animal = "Cat"
print(animal)
}
print(animal)
}
Task {
await testAsync()
}
This piece of code results in an error
Mutation of captured var 'animal' in concurrently-executing code
However, if you move the animal variable away from the context of this async function,
import Foundation
var animal = "Dog"
func testAsync() async {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
animal = "Cat"
print(animal)
}
print(animal)
}
Task {
await testAsync()
}
it will compile. I understand this error is to prevent data races but why does moving the variable make it safe?

Regarding the behavior of the globals example, I might refer you to Rob Napier’s comment re bugs/limitations related to the sendability of globals:
The compiler has many limitations in how it can reason about global variables. The short answer is “don't make global mutable variables.” It‘s come up on the forums, but hasn‘t gotten any discussion. https://forums.swift.org/t/sendability-checking-for-global-variables/56515
FWIW, if you put this in an actual app and change the “Strict Concurrency Checking” build setting to “Complete” you do receive the appropriate warning in the global example:
Reference to var 'animal' is not concurrency-safe because it involves shared mutable state
This compile-time detection of thread-safety issues is evolving, with many new errors promised in Swift 6 (which is why they’ve given us this new “Strict Concurrency Checking” setting so we can start reviewing our code with varying levels of checks).
Anyway, you can use an actor to offer thread-safe interaction with this value:
actor AnimalActor {
var animal = "Dog"
func setAnimal(newAnimal: String) {
animal = newAnimal
}
}
func testAsync() async {
let animalActor = AnimalActor()
Task {
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
await animalActor.setAnimal(newAnimal: "Cat")
print(await animalActor.animal)
}
print(await animalActor.animal)
}
Task {
await testAsync()
}
For more information, see WWDC 2021’s Protect mutable state with Swift actors and 2022’s Eliminate data races using Swift Concurrency.
Note, in the above, I have avoided using GCD API. The asyncAfter was the old, GCD, technique for deferring some work while not blocking the current thread. But the new Task.sleep (unlike the old Thread.sleep) achieves the same behavior within the concurrency system (and offers cancelation capabilities). Where possible, we should avoid GCD API in Swift concurrency codebases.

When you declare the variable inside an async function, it becomes part of the structured concurrency. Hypothetically your testAsync function can be run from any context. The change to animal, however, is done on the main thread, which introduces a data race.
In the second example, the variable is declared globally and operates on the main thread*. The compiler doesn’t strictly check for concurrency on global variables.
*: Actually, it is not guaranteed to run on the main thread. Like #Rob said, avoid using global variables.

Related

Use NWPathMonitor with Swift Modern Concurrency (AsyncStream) vs GCD (DispatchQueue)

I have noticed that the start(queue:) method in NWPathMonitor requires a queue of type DispatchQueue. Is there a way to implement this using Swift Modern Concurrency, probably using AsyncStream?
Using Apple documentation for AsyncStream, I have created the extension to NWPathMonitor, but I cannot start the NWPathMonitor monitor, any suggestion will be appreciated, thanks
extension NWPathMonitor {
static var nwpath: AsyncStream<NWPath> {
AsyncStream { continuation in
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
continuation.yield(path)
}
continuation.onTermination = { #Sendable _ in
monitor.cancel()
}
// monitor.start(queue: )
}
}
}
Read Apple's documentation
If you are wrapping legacy APIs within some continuation pattern (whether with AsyncStream or withCheckedContinuation or whatever), that wrapped code will have to employ whatever pattern the legacy API requires.
So, in short, if an API wrapped by an AsyncStream requires a dispatch queue, then simply supply it a queue.
So:
extension NWPathMonitor {
func paths() -> AsyncStream<NWPath> {
AsyncStream { continuation in
pathUpdateHandler = { path in
continuation.yield(path)
}
continuation.onTermination = { [weak self] _ in
self?.cancel()
}
start(queue: DispatchQueue(label: "NSPathMonitor.paths"))
}
}
}
Then you can do things like:
func startMonitoring() async {
let monitor = NWPathMonitor()
for await path in monitor.paths() {
print(path.debugDescription)
}
}
A few unrelated and stylistic recommendations, which I integrated in the above:
I did not make this static, as we generally want our extensions to be as flexible as possible. If this is in an extension, we want the application developer to create whatever NWPathMonitor they want (e.g., perhaps requiring or prohibiting certain interfaces) and then create the asynchronous sequence for the updates for whatever path monitor they want.
I made this a function, rather than a computed property, so that it is intuitive to an application developer that this will create a new sequence every time you call it. I would advise against hiding factories behind computed properties.
The concern with a computed property is that it is not at all obvious to an application developer unfamiliar with the underlying implementation that if you access the same property twice that you will get two completely different objects. Using a method makes this a little more explicit.
Obviously, you are free to do whatever you want regarding these two observations, but I at least wanted to explain my rationale for the adjustments in the above code.

Cannot safely access globalActor's method from a closure marked with same globalActor

#globalActor
actor LibraryAccount {
static let shared = LibraryAccount()
var booksOnLoan: [Book] = [Book()]
func getBook() -> Book {
return booksOnLoan[0]
}
}
class Book {
var title: String = "ABC"
}
func test() async {
let handle = Task { #LibraryAccount in
let b = await LibraryAccount.shared.getBook() // WARNING and even ERROR without await (although function is not async)
print(b.title)
}
}
This code generates the following warning:
Non-sendable type 'Book' returned by call to actor-isolated instance method 'getBook()' cannot cross actor boundary
However, the closure is itself marked with the same global Actor, there should be no Actor boundary here. Interestingly, when removing the await from the offending line, it will emit an error:
Expression is 'async' but is not marked with 'await'
This looks like it does not recognize that this is the same instance of the Actor as the one guarding the closure.
What's going on here? Am I misunderstanding how GlobalActors work or is this a bug?
Global actors aren't really designed to be used this way.
The type on which you mark #globalActor, is just a marker type, providing a shared property which returns the actual actor instance doing the synchronisation.
As the proposal puts it:
A global actor type can be a struct, enum, actor, or final class. It is essentially just a marker type that provides access to the actual shared actor instance via shared. The shared instance is a globally-unique actor instance that becomes synonymous with the global actor type, and will be used for synchronizing access to any code or data that is annotated with the global actor.
Therefore, what you write after static let shared = doesn't necessarily have to be LibraryAccount(), it can be any actor in the world. The type marked as #globalActor doesn't even need to be an actor itself.
So from Swift's perspective, it's not at all obvious that the LibraryAccount global actor is the same actor as any actor-typed expression you write in code, like LibraryAccount.shared. You might have implemented shared so that it returns a different instance of LibraryAccount the second time you call it, who knows? Static analysis only goes so far.
What Swift does know, is that two things marked #LibraryAccount are isolated to the same actor - i.e. this is purely nominal. After all, the original motivation for global actors was to easily mark things that need to be run on the main thread with #MainActor. Quote (emphasis mine):
The primary motivation for global actors is the main actor, and the
semantics of this feature are tuned to the needs of main-thread
execution. We know abstractly that there are other similar use cases,
but it's possible that global actors aren't the right match for those
use cases.
You are supposed to create a "marker" type, with almost nothing in it - that is the global actor. And isolate your business logic to the global actor by marking it.
In your case, you can rename LibraryAccount to LibraryAccountActor, then move your properties and methods to a LibraryAccount class, marked with #LibraryAccountActor:
#globalActor
actor LibraryAccountActor {
static let shared = LibraryAccountActor()
}
#LibraryAccountActor
class LibraryAccount {
static let shared = LibraryAccount()
var booksOnLoan: [Book] = [Book()]
func getBook() async -> Book { // this doesn't need to be async, does it?
return booksOnLoan[0]
}
}
class Book {
var title: String = "ABC"
}
func test() async {
let handle = Task { #LibraryAccountActor in
let b = await LibraryAccount.shared.getBook()
print(b.title)
}
}

What is the Swift concurrency equivalent to a promise–resolver pair?

With the PromiseKit library, it’s possible to create a promise and a resolver function together and store them on an instance of a class:
class ExampleClass {
// Promise and resolver for the top news headline, as obtained from
// some web service.
private let (headlinePromise, headlineSeal) = Promise<String>.pending()
}
Like any promise, we can chain off of headlinePromise to do some work once the value is available:
headlinePromise.get { headline in
updateUI(headline: headline)
}
// Some other stuff here
Since the promise has not been resolved yet, the contents of the get closure will be enqueued somewhere and control will immediately move to the “some other stuff here” section; updateUI will not be called unless and until the promise is resolved.
To resolve the promise, an instance method can call headlineSeal:
makeNetworkRequest("https://news.example/headline").get { headline in
headlineSeal.fulfill(headline)
}
The promise is now resolved, and any promise chains that had been waiting for headlinePromise will continue. For the rest of the life of this ExampleClass instance, any promise chain starting like
headlinePromise.get { headline in
// ...
}
will immediately begin executing. (“Immediately” might mean “right now, synchronously,” or it might mean “on the next run of the event loop”; the distinction isn’t important for me here.) Since promises can only be resolved once, any future calls to headlineSeal.fulfill(_:) or headlineSeal.reject(_:) will be no-ops.
Question
How can this pattern be translated idiomatically into Swift concurrency (“async/await”)? It’s not important that there be an object called a “promise” and a function called a “resolver”; what I’m looking for is a setup that has the following properties:
It’s possible for some code to declare a dependency on some bit of asynchronously-available state, and yield until that state is available.
It’s possible for that state to be “fulfilled” from potentially any instance method.
Once the state is available, any future chains of code that depend on that state are able to run right away.
Once the state is available, its value is immutable; the state cannot become unavailable again, nor can its value be changed.
I think that some of these can be accomplished by storing an instance variable
private let headlineTask: Task<String, Error>
and then waiting for the value with
let headline = try await headlineTask.value
but I’m not sure how that Task should be initialized or how it should be “fulfilled.”
Here is a way to reproduce a Promise which can be awaited by multiple consumers and fulfilled by any synchronous code:
public final class Promise<Success: Sendable>: Sendable {
typealias Waiter = CheckedContinuation<Success, Never>
struct State {
var waiters = [Waiter]()
var result: Success? = nil
}
private let state = ManagedCriticalState(State())
public init(_ elementType: Success.Type = Success.self) { }
#discardableResult
public func fulfill(with value: Success) -> Bool {
return state.withCriticalRegion { state in
if state.result == nil {
state.result = value
for waiters in state.waiters {
waiters.resume(returning: value)
}
state.waiters.removeAll()
return false
}
return true
}
}
public var value: Success {
get async {
await withCheckedContinuation { continuation in
state.withCriticalRegion { state in
if let result = state.result {
continuation.resume(returning: result)
} else {
state.waiters.append(continuation)
}
}
}
}
}
}
extension Promise where Success == Void {
func fulfill() -> Bool {
return fulfill(with: ())
}
}
The ManagedCriticalState type can be found in this file from the SwiftAsyncAlgorithms package.
I think I got the implementation safe and correct but if someone finds an error I'll update the answer. For reference I got inspired by AsyncChannel and this blog post.
You can use it like this:
#main
enum App {
static func main() async throws {
let promise = Promise(String.self)
// Delayed fulfilling.
let fulfiller = Task.detached {
print("Starting to wait...")
try await Task.sleep(nanoseconds: 2_000_000_000)
print("Promise fulfilled")
promise.fulfill(with: "Done!")
}
let consumer = Task.detached {
await (print("Promise resolved to '\(promise.value)'"))
}
// Launch concurrent consumer and producer
// and wait for them to complete.
try await fulfiller.value
await consumer.value
// A promise can be fulfilled only once and
// subsequent calls to `.value` immediatly return
// with the previously resolved value.
promise.fulfill(with: "Ooops")
await (print("Promise still resolved to '\(promise.value)'"))
}
}
Short explanation
In Swift Concurrency, the high-level Task type resembles a Future/Promise (it can be awaited and suspends execution until resolved) but the actual resolution cannot be controlled from the outside: one must compose built-in lower-level asynchronous functions such as URLSession.data() or Task.sleep().
However, Swift Concurrency provides a (Checked|Unsafe)Continuation type which basically act as a Promise resolver. It is a low-lever building block which purpose is to migrate regular asynchronous code (callback-based for instance) to the Swift Concurrency world.
In the above code, continuations are created by the consumers (via the .value property) and stored in the Promise. Later, when the result is available the stored continuations are fulfilled (with .resume()), which resumes the execution of the consumers. The result is also cached so that if it is already available when .value is called it is directly returned to the called.
When a Promise is fulfilled multiple times, the current behavior is to ignore subsequent calls and to return aa boolean value indicating if the Promise was already fulfilled. Other API's could be used (a trap, throwing an error, etc.).
The internal mutable state of the Promise must be protected from concurrent accesses since multiple concurrency domains could try to read and write from it at the same time. This is achieve with regular locking (I believe this could have been achieved with an actor, though).

What's difference between `add(_)` and `add(_) async`?

I don't understand what's the difference between add(_) and add(_) async method. Like the below code, the MyActor has two add methods and one of them uses async keyword. They can exist at the same time. If I comment out the second add method it will output AAAA. If both add methods exist at the same time, output "BBBBB"。
actor MyActor {
var num: Int = 0
func add(_ value: Int) {
print("AAAAA")
num += value
}
func add(_ value: Int) async {
print("BBBBB")
num += value
}
}
let actor = MyActor()
Task {
await actor.add(200)
print(await actor.num)
}
Supplementary:
With the second add method commented out, I defined let actor = MyActor() outside Task and I noticed the add method signed as add(_:). If move let actor = MyActor() inside Task the add method signed as add(_:) async
The difference emerges inside the actor, for example
actor MyActor {
func addSync(_ value: Int) {
}
func addAsync(_ value: Int) async {
}
func f() {
addSync(0)
}
func g() async {
addSync(0)
await addAsync(0)
}
}
In the actor method f and g you can call addSync synchronously. While outside the actor, you need always call an actor method with the await keyword as if the method is asynchronous:
func outside() async {
let actor = MyActor()
await actor.addSync(0)
}
Async in Swift allows for structured concurrency, which will improve the readability of complex asynchronous code. Completion closures are no longer needed, and calling into multiple asynchronous methods after each other is a lot more readable.
Async stands for asynchronous and can be seen as a method attribute making it clear that a method performs asynchronous work. An example of such a method looks as follows:
func fetchImages() async throws -> [UIImage] {
// .. perform data request
}
The fetchImages method is defined as async throwing, which means that it’s performing a failable asynchronous job. The method would return a collection of images if everything went well or throws an error if something went wrong.
How async replaces closure completion callbacks
Async methods replace the often seen closure completion callbacks. Completion callbacks were common in Swift to return from an asynchronous task, often combined with a Result type parameter. The above method would have been written as followed:
func fetchImages(completion: (Result<[UIImage], Error>) -> Void) {
// .. perform data request
}
Defining a method using a completion closure is still possible in Swift today, but it has a few downsides that are solved by using async instead:
You have to make sure yourself to call the completion closure in each possible method exit. Not doing so will possibly result in an app waiting for a result endlessly.
Closures are harder to read. It’s not as easy to reason about the order of execution as compared to how easy it is with structured concurrency.
Retain cycles need to be avoided using weak references.
Implementors need to switch over the result to get the outcome. It’s not possible to use try catch statements from the implementation level.
These downsides are based on the closure version using the relatively new Result enum. It’s likely that a lot of projects still make use of completion callbacks without this enumeration:
func fetchImages(completion: ([UIImage]?, Error?) -> Void) {
// .. perform data request
}
Defining a method like this makes it even harder to reason about the outcome on the caller’s side. Both value and error are optional, which requires us to perform an unwrap in any case. Unwrapping these optionals results in more code clutter which does not help to improve readability.

Concurrency issues with observable.observeOn() and common resources

I have an observable inside a function.
The function happens in a certain queue, queueA, and the observable is subscribed to with observeOn(schedulerB). In onNext, I'm changing a class variable.
In another function, I'm changing the same class variable, from a different queue.
Here is some code to demonstrate my situation:
class SomeClass {
var commonResource: [String: String] = [:]
var queueA = DispatchQueue(label: "A")
var queueB = DispatchQueue(label: "B")
var schedulerB = ConcurrentDispatchQueueScheduler(queue: QueueB)
func writeToResourceInOnNext() {
let obs: PublishSubject<String> = OtherClass.GetObservable()
obs.observeOn(schedulerB)
.subscribe(onNext: { [weak self] res in
// this happens on queue B
self.commonResource["key"] = res
}
}
func writeToResource() {
// this happens on queue A
commonResource["key"] = "otherValue"
}
}
My question is, is it likely to have concurrency issues, if commonResource is modified in both places at the same time?
What is the common practice for writing/reading from class/global variables inside onNext in an observable with observeOn?
Thanks all!
Since your SomeClass has no control over when these functions will be called or on what threads the answer is yes, you are setup to have concurrency issues in this code due to its passive nature.
The obvious solution here is to dispatch to queue B inside writeToResource() in order to avoid the race condition.
Another option would be to use an NSLock (or NSRecursiveLock) and lock it before you write to the resource and unlock it after.
The best practice is: when you have a side effect happening inside a subscribe function's closure (in this case writing to commonResource that the closure is the only place where the side effect occurs. This would mean doing away with the passive writeToResource() function and instead passing in an Observable that was generated by whatever code currently is calling the function.