Printed on: February 16, 2024
Swift’s present concurrency mannequin leverages duties to encapsulate the asynchronous work that you just’d prefer to carry out. I wrote in regards to the completely different sorts of duties we have now in Swift up to now. You’ll be able to check out that submit right here. On this submit, I’d prefer to discover the principles that Swift applies when it determines the place your duties and features run. Extra particularly, I’d prefer to discover how we are able to decide whether or not a job or perform will run on the primary actor or not.
We’ll begin this submit by very briefly taking a look at duties and the way we are able to decide the place they run. I’ll dig proper into the small print so in case you’re not fully updated on the fundamentals of Swift’s unstructured and indifferent duties, I extremely advocate that you just catch up right here.
After that, we’ll take a look at asynchronous features and the way we are able to cause about the place these features run.
To comply with together with this submit, it’s beneficial that you just’re considerably updated on Swift’s actors and the way they work. Check out my submit on actors if you wish to ensure you’ve acquired an important ideas down.
Reasoning about the place a Swift Process will run
In Swift, we have now two sorts of duties:
- Unstructured duties
- Indifferent duties
Every job sort has its personal guidelines concerning the place the duty will run its physique.
While you create a indifferent job, this job will at all times run its physique utilizing the worldwide executor. In sensible phrases which means that a indifferent job will at all times run on a background thread. You’ll be able to create a indifferent job as follows:
Process.indifferent {
// this runs on the worldwide executor
}
A indifferent job ought to rarely be utilized in apply as a result of there are different methods to carry out work within the background that don’t contain beginning a brand new job (that doesn’t take part in structured concurrency).
The opposite approach to begin a brand new job is by creating an unstructured job. This appears to be like as follows:
Process {
// this runs ... someplace?
}
An unstructured job will inherit sure issues from its context, like the present actor for instance. It’s this present actor that determines the place our unstructured job will run.
Generally it’s fairly apparent that we wish a job to run on the primary actor:
Process { @MainActor in
}
Whereas this job inherits an actor from the present context, we’re overriding this by annotating our job physique with MainActor
to make it possible for our job’s physique runs on the primary actor.
Fascinating sidenote: you are able to do the identical with a indifferent job.
Moreover, we are able to create a brand new job that’s on the primary actor like this:
@MainActor
struct MyView: View {
// physique and many others...
func startTask() {
Process {
// this job runs on the primary actor
}
}
}
Our SwiftUI view on this instance is annotated with @MainActor
. Which means that each perform and property that’s outlined on MyView
will probably be executed on the primary actor. Together with our startTask
perform. The Process
inherits the primary actor from MyView
so it’s working its physique on the primary actor.
If we make one small change to the view, the whole lot adjustments:
struct MyView: View {
// physique and many others...
func startTask() {
Process {
// this job mightrun on the primary actor
}
}
}
As an alternative of understanding that startTask
will run on the primary actor, we’re no longer so certain about the place this job will run. The rationale for that is that it now is dependent upon the place we name startTask
from. If we name startTask
from a job that we’ve outlined on view’s physique utilizing the job
view modifier, we’re working in a predominant actor context as a result of the duty that’s created by the view modifier is related to the primary actor.
If we name startTask
from a non-main actor remoted spot, like a indifferent job or an asynchronous perform then our job physique will run on the worldwide executor. Even calling startTask
from a button motion will trigger the Process
to run on the worldwide executor.
At runtime, the one approach to check this that I do know off is to make use of the deprecated Thread.isMainThread
property or to place a breakpoint in your job’s physique after which see which thread your program pauses on.
As a rule of thumb you can say {that a} Process
will at all times run within the background in case you’re not hooked up to any actors. That is the case once you create a brand new Process
from any object that’s not predominant actor annotated for instance. While you create your job from a spot that’s predominant actor annotated, you understand your job will run on the primary actor.
Sadly, this isn’t at all times easy to find out and Apple appears to need us to not fear an excessive amount of about this.
Fortunately, the best way async features work in Swift can provide us some confidence in ensuring that we don’t block the primary actor by chance.
Reasoning about the place an async perform runs in Swift
Everytime you wish to name an async perform in Swift, it’s important to do that from a job and it’s important to do that from inside an present asynchronous context. Should you’re not but in an async perform you’ll normally create this asynchronous context by making a brand new Process
object.
From inside that job you’ll name your async perform and prefix the decision with the await
key phrase. It’s a typical false impression that once you await
a perform name the duty you’re utilizing the await
from will probably be blocked till the perform you’re ready for is accomplished. If this had been true, you’d at all times wish to ensure that your duties run away from the primary actor to ensure you’re not blocking the primary actor when you’re ready for one thing like a community name to finish.
Fortunately, awaiting one thing does not block the present actor. As an alternative, it units apart all work that’s ongoing in order that the actor you had been on is free to carry out different work. I gave a chat the place I went into element on this. You’ll be able to see the speak right here.
Understanding all of this, let’s speak about how we are able to decide the place an async perform will run. Look at the next code:
struct MyView: View {
// physique and many others...
func performWork() async {
// Can we decide the place this perform runs?
}
}
The performWork
perform is marked async
which signifies that we should name it from inside an async context, and we have now to await it.
An inexpensive assumption could be to anticipate this perform to run on the actor that we’ve referred to as this perform from.
For instance, within the following scenario you may anticipate performWork
to run on the primary actor:
struct MyView: View {
var physique: some View {
Textual content("Pattern...")
.job {
await peformWork()
}
}
func performWork() async {
// Can we decide the place this perform runs?
}
}
Curiously sufficient, peformWork
will not run on the primary actor on this case. The rationale for that’s that in Swift, features don’t simply run on no matter actor they had been referred to as from. As an alternative, they run on the worldwide executor until instructed in any other case.
In sensible phrases, which means that your asynchronous features will must be both straight or not directly annotated with the primary actor if you need them to run on the primary actor. In each different scenario your perform will run on the worldwide executor.
Whereas this rule is easy sufficient, it may be difficult to find out precisely whether or not or not your perform is implicitly annotated with @MainActor
. That is normally the case when there’s inheritance concerned.
A less complicated instance appears to be like as follows:
@MainActor
struct MyView: View {
var physique: some View {
Textual content("Pattern...")
.job {
await peformWork()
}
}
func performWork() async {
// This perform will run on the primary actor
}
}
As a result of we’ve annotated our view with @MainActor
, the asynchronous performWork
perform inherits the annotation and it’ll run on the primary actor.
Whereas the apply of reasoning about the place an asynchronous perform will run isn’t easy, I normally discover this simpler than reasoning about the place my Process
will run nevertheless it’s nonetheless not trivial.
The bottom line is at all times to have a look at the perform itself first. If there’s no @MainActor
, you possibly can take a look at the enclosing object’s definition. After which you can take a look at base courses and protocols to ensure there isn’t any predominant actor affiliation there.
At runtime, you possibly can place a breakpoint or print the deprecated Thread.isMainThread
property to see in case your async perform runs on the primary actor. If it does, you’ll know that there’s some predominant actor annotation that’s utilized to your asynchronous perform. Should you’re not working on the primary actor, you possibly can safely say that there’s no predominant actor annotation utilized to your perform.
In Abstract
Swift Concurrency’s guidelines for figuring out the place a job or perform runs are comparatively clear and particular. Nevertheless, in apply issues can get somewhat muddy for duties as a result of it’s not at all times trivial to cause about whether or not or that your job is created from a context that’s related to the primary actor. Notice that working on the primary thread shouldn’t be the identical as being related to the primary actor.
For async features we are able to cause extra regionally which leads to a neater psychological modal nevertheless it’s nonetheless not trivial.
We will use Thread.isMainThread
and breakpoints to determine whether or not our code is working on the primary thread however these instruments aren’t excellent and we don’t have any higher alternate options in the intervening time (that I do know off).
When you’ve got any additions, questions, or feedback on this text please don’t hesitate to succeed in out on X.