Calling a method from an actor's init method - swift

I'm trying to convert one of my classes in Swift to an actor. The current init method of my class calls another instance method to do a bunch of initialization work. After converting, here's a simplified version of my actor:
actor MyClass {
private let name: String
init(name: String) {
self.name = name
self.initialize() // Error on this line
}
private func initialize() {
// Do some work
}
func login() {
self.initialize()
// Do some work
}
// Bunch of other methods
}
I get the following error when I try to compile:
Actor-isolated instance method 'initialize()' can not be referenced from a non-isolated context; this is an error in Swift 6
I found that I can replace self.initialize() with:
Task { await self.initialize() }
Is that the best way to do this? Could this cause any race conditions where an external caller can execute a method on my actor before the initialize() method has a chance to run? Seems cumbersome that you are not in the isolated context in the actor's init method. I wasn't able to find any Swift documentation that explained this.

I wasn't able to find any Swift documentation that explained this.
This is explained in the proposal SE-0327, in this section (emphasis mine):
An actor's executor serves as the arbiter for race-free access to the
actor's stored properties, analogous to a lock. A task can access an
actor's isolated state if it is running on the actor's executor. The
process of gaining access to an executor can only be done
asynchronously from a task, as blocking a thread to wait for access is
against the ethos of Swift Concurrency. This is why invoking a
non-async method of an actor instance, from outside of the actor's
isolation domain, requires an await to mark the possible suspension.
The process of gaining access to an actor's executor will be referred
to as "hopping" onto the executor throughout this proposal.
Non-async initializers and all deinitializers of an actor cannot hop
to an actor's executor, which would protect its state from concurrent
access by other tasks. Without performing a hop, a race between a new
task and the code appearing in an init can happen:
actor Clicker {
var count: Int
func click() { self.count += 1 }
init(bad: Void) {
self.count = 0
// no actor hop happens, because non-async init.
Task { await self.click() }
self.click() // đź’Ą this mutation races with the task!
print(self.count) // đź’Ą Can print 1 or 2!
}
}
To prevent the race above in init(bad:), Swift 5.5 imposed a restriction on what can be done with self in a non-async initializer. In particular, having self escape in a closure capture, or be passed (implicitly) in the method call to click, triggers a warning that such uses of self will be an error in Swift 6.
They then went on to give another example that should have been accepted by the compiler, and suggested to loosen those restriction. In either case though, it does not apply to your code. See this section if you want the details about why it still does not compile in newer versions of Swift.
So the solution is, as the first quote says, mark the init as async. This way the initialiser would be effectively run on the actor's executor. The caller would use await when initialising, to "hop" in.

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.

What priority will as Task run as when the operation has the #MainActor decorator?

I'm curious what priority the async block() in the code snippet below will run as when the Task's operation is marked #MainActor?
According to the DispatchQueue docs the main queue always runs with a qos of .userInteractive. So I assume the Task's #MainActor operation will also run as .userInteractive - there's only 1 main thread after all.
But since we requested a custom priority for the Task, what priority will the async block run with?
Task<Void, Never>(priority: priority) { #MainActor in
doStuffOnMainQueue() // .userInteractive
await block() // priority?
}
When you add #MainActor qualifier to the closure, that obviously runs on the main actor. But when it reaches await block(), that translates to “suspend the current actor (i.e., the main actor in this case) so it’s free to do other stuff and await the results of the block().”
Regarding the question of what priority the block() uses, that depends on how it was declared. Assuming block() is just a closure that you passed to this method, then in my experience it runs with the priority that you supplied to Task, not the main actor. But there are lots of variables (e.g., you could specify #MainActor for that closure parameter, too; you could be calling some method running on some other actor; etc.), so we cannot get more specific without seeing a reproducible example.
But, bottom line, when you await something, the actor/priority of the awaited code is dictated by how you defined that, rather than the context of the code from which you called it.

NS_SWIFT_UI_ACTOR annotation not working with async variants of callback based objective-C methods

In Objective-c we can use NS_SWIFT_UI_ACTOR to indicate that a method or a whole class should be executed on the main actor (as we would do with #MainActor in Swift). This works correctly since, when invoking a method which returns a value from outside the main actor (using a Task.detached with background priority), the compiler forces us to call it using "await" (to allow for "actor switch") and, at runtime, I can see that the code is executed on the main thread.
However, when calling a callback based method on the same class (which gets an automatically "imported" async variant, as explained here) the NS_SWIFT_UI_ACTOR seems not working properly, and the code is executed on a background thread.
NS_SWIFT_UI_ACTOR
#interface ObjcClass : NSObject
- (NSString *)method1;
- (void)method2WithCompletion: (void (^)(NSString * _Nonnull))completion;
#end
Task.detached(priority: .background) {
let objcInstance = ObjcClass()
print(await objcInstance.method1()) <----- this is called on the main thread
print(await objcInstance.method2()) <----- this is called on a bg thread
}
While I understand what your expectation is I don't think as of now the way actor is implemented there is any way to resolve this. Actors in swift are reentrant, which means whenever you invoke any async method inside actor's method/actor task, actor suspends the current task until the method returns and starts processing other tasks. This reentrancy behavior is important as it doesn't just lock the actor when executing a long-running task.
Let's take an example of a MainActor isolated async method:
func test() async {
callSync() // Runs on MainActor
await callAsync() // Suspends to process other task, waits for return to resume executing on MainActor
callSync() // Runs on MainActor
}
But since swift can't infer from an objective c async method which part should run on MainActor and for which part actor can just suspend the current task to process other tasks, the entire async method isn't isolated to the actor.
However, there are proposals currently to allow customizing an actor's reentrancy so you might be able to execute the entire objc async method on the actor.

Difference of creating regular or detached Task from task-less context?

When creating a Task in a task-less context (e.g. from some random AppKit code), is there a difference between creating a detached or a regular task? For example, calling
Task.detached
or Task.init
Since the task has no actor to inherit from, I figured those 2 calls must be equivalent or are there still implementation differences to consider?
First of all, it is important to note that neither of these create child tasks. A child task is created by structured concurrency constructs like awaiting an async function, async let, or task groups, as opposed to unstructured concurrency like Task.init and Task.detached.
In addition to the structured approaches to concurrency described in the previous sections, Swift also supports unstructured concurrency. [...] To create an unstructured task that runs on the current actor, call the Task.init(priority:operation:) initializer. To create an unstructured task that’s not part of the current actor, known more specifically as a detached task, call the Task.detached(priority:operation:) class method.
This is also documented in Task.init (my bold)
Runs the given nonthrowing operation asynchronously as part of a new top-level task on behalf of the current actor.
You can also see this in action - a child task created by awaiting an async function is cancelled when its parent is cancelled, but a task created by Task.init is not.
let task = Task {
let notChild = Task {
await Task.sleep(1_000_000_000)
if !Task.isCancelled {
print("Not Child!")
} else {
print("Child!")
}
}
}
task.cancel()
// prints "Not Child!"
func childTask() async {
await Task.sleep(1_000_000_000)
if !Task.isCancelled {
print("Not Child!")
} else {
print("Child!")
}
}
let task = Task {
await childTask()
}
task.cancel()
// prints "Child!"
Anyway, back to your question, so what actually is the difference between Task.init and Task.detached? The first quote in my answer addressed that a little bit, and this is also mentioned in the documentation of Task.init too:
Like Task.detached(priority:operation:), this function creates a separate, top-level task. Unlike Task.detached(priority:operation:), the task created by Task.init(priority:operation:) inherits the priority and actor context of the caller, so the operation is treated more like an asynchronous extension to the synchronous operation.
Essentially, it's about the actor on which the task is executed, so even if there is no current task, but there is a current actor, Task.init and Task.detached will still do different things. Task.init will run the task on the current actor, whereas Task.detached will run the task detached from any actor.
See also: https://www.hackingwithswift.com/quick-start/concurrency/whats-the-difference-between-a-task-and-a-detached-task

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.