Concatenating two observable sequences that both have subscribeOn. How do I ensure my observable runs on a thread? - swift

When it comes to enforcing that a certain piece of Observable.create code runs in a specific thread (i.e. background thread), i worry that using the subscribeOn operator might not work because there are times that I might chain this observable sequence to another observable sequence that runs on a main thread (using observeOn).
Example
The situation is that I have an Observable sequence running on the main thread (i.e. an alert box asking the user for input, as to whether perform the network call or not).
Would it be better to ensure that this Observable.create code runs in the background thread by having something like:
Observable<String>.empty()
.observeOn(ConcurrentMainScheduler(queue: background.queue))
.concat(myObservableNetworkCall)
Why not just use subscribeOn?
The problem is if I had used subscribeOn (second) and the previous observable (the alert controller) was set to run on the background thread using subscribeOn (first), then the second subscribeOn operator would not work since the first call is closer to the source observable:
If you specify multiple subscribeOn() operators, the one closes to the source (the left-most), will be the one used.
Thomas Nield on RxJava's subscribeOn and observeOn operators (February 2016)
That may be the behavior for RxJava, but I am not sure for Swift. Reactivex.io simply says that we should not call subscribeOn multiple times.
I tend to wrap operations into Observable<Void>s and they need to be run on different threads... That is why I am asking for how to ensure an Observable code run in the thread I specified it to. subscribeOn wouldn't work because I can concatenate the observable.
I want the thread it should run in to be encapsulated in my Observable definition, not higher up in the chain.
Is the best practice to do the following:
Start with an Observable.empty using the data type I wish to use.
Use observeOn to force the thread that I want it to run in.
Concatenate it with the actual Observable that I want to use.
Edit
I have read the subscribeOn and observeOn documentation on reactivex.io.
I'm familiar with how to switch between threads using subscribeOn and observeOn.
What I'm specifically concerned about is the complication of using subscribeOn when concatenating or combining observable sequences.
The problem is, the observables need to run specifically on one thread, AND they don't know where and who they'll be concatenated with. Since I know exactly which thread they should run on, I'd prefer to encapsulate the scheduler definition within the observable's definition instead of when I'm chaining a sequence.

In the function declaration it is better not to specify on which thread the function is to be called.
For instance:
func myObservableNetworkCall() -> Observable<String> {
return Observable<String>.create { observer in
// your network code here
return Disposables.create {
// Your dispose
}
}
}
func otherObservableNetworkCall(s: String) -> Observable<String> {
return Observable<String>.create { observer in
// your network code here
return Disposables.create {
// Your dispose
}
}
}
And then switch between Schedulers:
myObservableNetworkCall()
.observeOn(ConcurrentMainScheduler(queue: background.queue)) // .background thread, network request, mapping, etc...
.flatMap { string in
otherObservableNetworkCall(s: string)
}
.observeOn(MainScheduler.instance) // switch to MainScheduler, UI updates
.subscribe(onNext:{ string in
// do something
})

Related

Best practice with asynchronous functions Swift & Combine

I'm converting my Swift app to use Combine as well as async/await and I'm trying to understand what's the best way to handle interactions between asynchronous functions and the main thread.
Here's an asynchronous function that loads a user:
class AccountManager {
static func fetchOrLoadUser() async throws -> AppUser {
if let user = AppUser.current.value {
return user
}
let syncUser = try await loadUser()
let user = try AppUser(syncUser: syncUser)
AppUser.current.value = user // [warning]: "Publishing changes from background threads is not allowed"
return user
}
}
And a class:
class AppUser {
static var current = CurrentValueSubject<AppUser?,Never>(nil)
// ...
}
Note: I chose to use CurrentValueSubject because it allows me to both (1) read this value synchronously whenever I need it and (2) subscribe for changes.
Now, on the line marked above I get the error Publishing changes from background threads is not allowed, which I understand. I see different ways to solve this issue:
1. Mark whole AccountManager class as #MainActor
Since most of the work done in asynchronous functions is to wait for network results, I'm wondering if there is an issue with simply running everything on the main thread. Would that cause performance issues or not?
2. Englobe error line in DispatchQueue.main.sync
Is that a reasonable solution, or would that cause threading problems like deadlocks?
3. Use DispatchGroup with enter(), leave() and wait()
Like in this answer. Is there a difference at all with solution #2? Because this solution needs more lines of code so I'd rather not use it if possible —I prefer clean code.
You can wrap the call in an await MainActor.run { } block. I think this is the most Swifty way of doing that.
You should not use Dispatch mechanism while using Swift Concurrency, event though I think DispatchQueue.main.async { } is safe to use here.
The #MainActor attribute is safe but shouldn’t be used on anObservableObject and could potentially slow down the UI if CPU-bound code is run in a method of the annotated type.

Why does a Task within a #MainActor not block the UI?

Today I refactored a ViewModel for a SwiftUI view to structured concurrency. It fires a network request and when the request comes back, updates a #Published property to update the UI. Since I use a Task to perform the network request, I have to get back to the MainActor to update my property, and I was exploring different ways to do that. One straightforward way was to use MainActor.run inside my Task, which works just fine. I then tried to use #MainActor, and don't quite understand the behaviour here.
A bit simplified, my ViewModel would look somewhat like this:
class ContentViewModel: ObservableObject {
#Published var showLoadingIndicator = false
#MainActor func reload() {
showLoadingIndicator = true
Task {
try await doNetworkRequest()
showLoadingIndicator = false
}
}
#MainActor func someOtherMethod() {
// does UI work
}
}
I would have expected this to not work properly.
First, I expected SwiftUI to complain that showLoadingIndicator = false happens off the main thread. It didn't. So I put in a breakpoint, and it seems even the Task within a #MainActor is run on the main thread. Why that is is maybe a question for another day, I think I haven't quite figured out Task yet. For now, let's accept this.
So then I would have expected the UI to be blocked during my networkRequest - after all, it is run on the main thread. But this is not the case either. The network request runs, and the UI stays responsive during that. Even a call to another method on the main actor (e.g. someOtherMethod) works completely fine.
Even running something like Task.sleep() within doNetworkRequest will STILL work completely fine. This is great, but I would like to understand why.
My questions:
a) Am I right in assuming a Task within a MainActor does not block the UI? Why?
b) Is this a sensible approach, or can I run into trouble by using #MainActor for dispatching asynchronous work like this?
await is a yield point in Swift. It's where the current Task releases the queue and allows something else to run. So at this line:
try await doNetworkRequest()
your Task will let go of the main queue, and let something else be scheduled. It won't block the queue waiting for it to finish.
This means that after the await returns, it's possible that other code has been run by the main actor, so you can't trust the values of properties or other preconditions you've cached before the await.
Currently there's no simple, built-in way to say "block this actor until this finishes." Actors are reentrant.

How can I avoid that my Swift async method runs on the main thread in SwiftUI?

I watched all the videos on async/await (and actors), but I am still confused a bit.
So assume I have an async method: func postMessage(_ message: String) async throws and I have a simple SwiftUI view.
#MainActor
struct ContentView: View {
#StateObject private var api = API()
var body: some View {
Button("Post Message") {
Task {
do {
try await api.postMessage("First post!")
} catch {
print(error)
}
}
}
}
}
Here I explicitly tell SwiftUI to use the #MainActor although I know it would have inferred from #StateObject.
To my understanding, since we use the #MainActor the work is done on the main thread. Meaning the work on Task would also be done on the main thread. Which is not what I want as the uploading process might take some while. In this case I could use Task.detached to use a different thread. Which would solve it. If my understanding is correct.
Now to make it a little more complicated. What if... postMessage would return a post identifier as an integer and I like to present that in the view?
struct ContentView: View {
#StateObject private var api = API()
#State private var identifier: Int = 0
var body: some View {
Text("Post Identifier: \(String(describing: identifier))")
Button("Post Message") {
Task {
do {
identifier = try await api.postMessage("First post!")
} catch {
identifier = 0
print(error)
}
}
}
}
}
This would work as (again to my understanding) Task is run on the main thread. If I would change it now to Task.detached we will get an error "Property 'identifier' isolated to global actor 'MainActor' can not be mutated from a non-isolated context".
Which makes sense, but how can we return the value to the main actor so the view can be updated?
Perhaps my assumptions are wrong. Let's look at my API class.
actor API {
func postMessage(_ message: String) async throws -> Int {
// Some complex internet code
return 0
}
}
Since the API runs in its own actor. Would the internet work also run on a different thread?
The question is very good, and answer is complex. I have spent significant time on this topic, for detailed information please follow the link to Apple developer forum in comments.
All tasks mentioned below are unstructured tasks, eg. created by Task ...
This is key: "The main actor is an actor that represents the main thread. The main actor performs all of its synchronization through the main dispatch queue.
Unstructured tasks created by Task.init eg. Task { ... } inherit the actor async context.
Detached tasks Task.detached, async let = tasks, group tasks do NOT inherit actor async context.
Example 1: let task1 = Task { () -> Void in ... } creates and starts new task that inherits priority and async context from the point, where it is called. When created on the main thread, the task will run on the main thread.
Example 2: let task1 = Task.detached { () -> Void in ... } creates and starts new task that does NOT inherit priority nor async context. The task will run on some thread, very likely on other thread than is the current thread. The executor decides that.
Example 3: let task1 = Task.detached { #MainActor () -> Void in ... } creates and starts new task that does NOT inherit priority nor async context, but the task will run on the main thread, because it is annotated so.
Very likely, the task will contain at least one await or async let = command. These commands are part of structured concurrency, and you cannot influence, on what thread are the implicitly created tasks (not discussed here at all) executed. The Swift executor decides that.
The inherited actor async context has nothing to do with threads, after each await the thread may change, however the actor async context is kept throughout all the unstructured task (yes, may be on various threads, but this is important just for the executor).
If the inherited actor async context is MainActor, then the task runs on the main thread, from beginning till its end, because the actor context is MainActor. This is important if you plan to run some really parallel computation - make sure all the unstructured tasks do not run on the same thread.
ContentView is on #MainActor in both the cases: in the first case is it #MainActor explicitely, the second case uses #StateObject property wrapper that is #MainActor, so the whole ContentView structure is #MainActor inferred. https://www.hackingwithswift.com/quick-start/concurrency/understanding-how-global-actor-inference-works
async let = is a structured concurrency, it does not inherit actor async context, and runs in parallel immediately, as scheduled by the executor (on some other thread, if available)
The example on top has one system flaw: #StateObject private var api = API() is #MainActor. This is forced by the #StateObject. So, I would recommend to inject other actor with other actor async context as a dependency without #StateObject. The async/await will really work, keeping await calls with the proper actor contexts.
You said:
To my understanding, since we use the #MainActor the work is done on the main thread. Meaning the work on Task would also be done on the main thread. Which is not what I want as the uploading process might take some while.
Just because the uploading process (a) might take some time; and (b) was invoked from the main actor, that does not mean that the main thread will be blocked while the request is performed.
In fact, this is the whole point of Swift concurrency’s await. It frees the current thread to go do other things while we await results from the called routine. Do not conflate the await of Swift concurrency (which will not block the calling thread) with the various wait constructs of traditional GCD patterns (which will).
E.g., consider the following code launched from the main actor:
Task {
do {
identifier = try await api.postMessage("First post!")
} catch {
identifier = 0
print(error)
}
}
Yes, because you used Task and invoked it from the main actor, that code will also run on the main actor. Thus, identifier will be updated on the main actor. But it says nothing about which actor/queue/thread that postMessage is using. The await keyword means “I'll suspend this path of execution, and let the main thread and go do other things while we await the postMessage initiating its network request and its eventual response.”
You asked:
Let's look at my API class.
actor API {
func postMessage(_ message: String) async throws -> Int {
// Some complex internet code
return 0
}
}
Since the API runs in its own actor. Would the internet work also run
on a different thread?
As a general rule, networking libraries perform their requests asynchronously, not blocking the threads from which they are called. Assuming the “complex internet code” follows this standard convention, you then you simply do not have to worry about what thread it is running on. The actor’s thread will not be blocked while the request runs.
Taking this a step further, the fact that the API is its own actor is immaterial to the question. Even if API was on the main actor, because the network request runs asynchronously, whatever the thread used by API will not be blocked.
(NB: The above assumes that the “complex internet code” has followed conventional asynchronous network programming patterns. We obviously cannot comment further without seeing a representative example of this code.)
See WWDC 2021 video Swift concurrency: Behind the scenes if you are interested in how Swift concurrency manages threads for us.
The Task is the bridge between the Async and the Synchronous world. The completion block of Button being the synchronous world and the postMessage method as being the asynchronous world.
The Task is run on the #MainActor however since we await the result of api.postMessage it may suspend and the main thread is not blocked.
Thanks to Quang Hà and Lorem Ipsum.
Useful information regarding this question from WWDC.
From Discover concurrency in SwiftUI (Timecode - 7:24)
To update correctly, SwiftUI needs these events to happen in order: objectWillChange, the ObservableObject’s state is updated, and then the run loop reaches its next tick. If I can ensure that these all happen on the main actor, I can guarantee this ordering. Prior to Swift 5.5, I might have dispatched back to the main queue to update my state, but now it’s much easier. Just use await! By using await to make an async call from the main actor, I let other work continue on the main actor while the async work happens. This is called “yielding” the main actor.
From Meet async/await in Swift (Timecode - 22:13):
The solution is to use the async task function. An async task packages up the work in the closure and sends it to the system for immediate execution on the next available thread, like the async function on a global dispatch queue.
From Protect mutable state with Swift actors (Timecode - 7:25)
If the actor is busy, then your code will suspend so that the CPU you're running on can do other useful work. When the actor becomes free again, it will wake up your code -- resuming execution -- so the call can run on the actor. The await keyword in this example indicates that the asynchronous call to the actor might involve such a suspension.
From Protect mutable state with Swift actors (Timecode - 23:47)
When you are building an app, you need to think about the main thread. It is where the core user interface rendering happens, as well as where user interaction events are processed. Operations that work with the UI generally need to be performed from the main thread. However, you don't want to do all of your work on the main thread. If you do too much work on the main thread, say, because you have some slow input/output operation or blocking interaction with a server, your UI will freeze. So, you need to be careful to do work on the main thread when it interacts with the UI but get off the main thread quickly for computationally expensive or long-waiting operations. So, we do work off the main thread when we can and then call DispatchQueue.main.async in your code whenever you have a particular operation that must be executed on the main thread.
If you know you're already running on the main thread, you can safely access and update your UI state. If you aren't running on the main thread, you need to interact with it asynchronously. This is exactly how actors work.
The main actor is an actor that represents the main thread. It differs from a normal actor. The main actor performs all of its synchronization through the main dispatch queue. This means that, from a runtime perspective, the main actor is interchangeable with using DispatchQueue.main.

GCD serial queue like approach using swift async/await api?

I am adopting the new async/await Swift API. Things work great.
Currently in my apps I am using GCD serial queues as a pipeline to enforce tasks to happen serially.
For manipulating data of some critical models in the app I use a serial queue accessible from anywhere in the app:
let modelQueue = DispatchQueue(label: "com.myapp.updatemodelqueue")
Anytime a model needs to modify some model data I dispatch to that queue:
modelQueue.async {
// ... model updates
}
With the new async/await changes I am making I still want to force model actual updates to happen serially. So, for example, when I import new model data from the server I want to make sure they happen serially.
For example, I may have a call in my model that looks like this:
func updateCarModel() async {
let data = await getModelFromServer()
modelQueue.async {
// update model
}
}
Writing that function using that pattern however would not wait for the model update changes because of the modelQueue.async. I do not want to use modelQueue.sync to avoid deadlocks.
So then after watching WWDC videos and looking at documentation I implemented this way, leveraging withCheckedContinuation:
func updateCarModel() async {
let data = await getModelFromServer()
await withCheckedContinuation({ continuation in
modelQueue.async {
// update model
continuation.resume()
}
})
}
However, to my understanding withCheckedContinuation is really meant to allow us to incrementally transition to fully adopt the new async/await Swift API. So, it does not seem to be what I should use as a final approach.
I then looked into actor, but I am not sure how that would allow me to serialize any model work I want to serialize around the app like I did with a static queue like shown above.
So, how can I enforce my model around the app to do model updates serially like I used to while also fully adopting the new await/async swift API without using withCheckedContinuation?
By making the model an actor, Swift synchronizes access to it' shared mutable state. If the model is written like this:
actor Model {
var data: Data
func updateModel(newData: Data) {
data = newData
}
}
The updateModel function here is synchronous, it's execution is uninterrupted after it's invoked. Because Model is an actor, Swift restricts you to treat it as if you are calling an asynchronous funtion from the outside. You'd have to await, which results in suspension of your active thread.
If in case you'd want to make updateModel async, the code within will always be synchronous unless if you explicitly suspend it by calling await. The order of execution of multiple updateModel calls is not very deterministic. As far as you don't suspend within the updateModel block, it is sure that they execute serially. In such case, there is no use making the updateModel async.
If your update model code is synchronous you can make your model actor type to synchronize access. Actors in swift behave similar to serial DispatchQueue, they perform only one task at a time in the order of submission. However, current swift actors are re-entrant, which means if you are calling any async method actor suspends the current task until the async function completes and proceeds to process other submitted tasks.
If your update code is asynchronous, using an actor might introduce data race. To avoid this, you can wait for non-reentrant actor support in swift. Or you can try this workaround TaskQueue I have created to synchronize between asynchronous tasks or use other synchronization mechanisms that are also created by me.

How can I freeze Kotlin objects created in Swift?

I’m using Kotlin-Native with native-mt coroutine support and the Ktor library. I have several suspended functions that take in an object built using a builder pattern. I understand I need to call the suspended function on the main/ui thread. However, I can’t guarantee that the builder objects will be created on that thread. My understanding is they would need to be frozen before sending them to the main thread to be called with the suspended function. Is that correct?
For instance, this would fail because the query object hasn’t been frozen:
func loadData() {
DispatchQueue.global(qos: .background).async {
let query = CustomerQuery().emails(value: ["customer#gmail.com"])
self.fetchCustomersAndDoSomething(query: query)
}
}
func fetchCustomersAndDoSomething(query: CustomerQuery) {
DispatchQueue.main.async {
self.mylibrary.getCustomers(query: query) { response, err in
// do something with response
}
}
}
If that’s true, am I correct that I would need to add a method to every such object in order to ‘freeze’ it, since the freeze() Kotlin function from Freezing.kt doesn’t seem to be accessible from the Swift code importing my library? This is further complicated by the fact that freezing only applies to the iOS code, as the Android code doesn't need it.
Is there a simpler way to pass in Kotlin objects created by Swift to a suspended function, without requiring that those objects be created on the main thread?
In the Kotlin/Native world, whenever you are sharing objects between threads you have to make sure they are frozen (immutable), if you are not planning on making them #ThreadLocal. Android is an exception, since JVM is not that strict, and let's you share mutable objects between threads.
You have two options:
Either expose a freeze() function and use that
freeze() every incoming object in your shared code
Also if you don't freeze, probably you'll bump into IncorrectDereferenceException, which means you are trying to share mutable/non-frozen state
You don't freeze Swift classes. If CustomerQuery is a Kotlin class, you would need to freeze that.
However, you only need to call suspend functions on the main thread if you rely on the auto-generated Objc interface from the Kotlin compiler. We generally recommend not doing that because you can't control the lifecycle, but that's a whole different discussion.