Understanding unstructured and indifferent duties in Swift – Donny Wals


Printed on: April 13, 2023

If you simply begin out with studying Swift Concurrency you’ll discover that there are a number of methods to create new duties. One strategy creates a mother or father / baby relationship between duties, one other creates duties which can be unstructured however do inherit some context and there’s an strategy that creates duties which can be utterly indifferent from all context.

On this submit, I’ll deal with unstructured and indifferent duties. If you happen to’re focused on studying extra about baby duties, I extremely suggest that you simply learn the next posts:

These two posts go in depth on the connection between mother or father and baby duties in Swift Concurrency, how cancellation propagates between duties, and extra.

This submit assumes that you simply perceive the fundamentals of structured concurrency which you’ll study extra about in this submit. You don’t should have mastered the subject of structured concurrency, however having some sense of what structured concurrency is all about will show you how to perceive this submit significantly better.

Creating unstructured duties with Activity.init

The commonest manner wherein you’ll be creating duties in Swift shall be with Activity.init which you’ll most likely write as follows with out spelling out the .init:

Activity {
  // carry out work
}

An unstructured activity is a activity that has no mother or father / baby relationship with the place it known as from, so it doesn’t take part in structured concurrency. As a substitute, we create a very new island of concurrency with its personal scopes and lifecycle.

Nonetheless, that doesn’t imply an unstructured activity is created completely unbiased from every little thing else.

An unstructured activity will inherit two items of context from the place it’s created:

  • The actor we’re presently operating on (if any)
  • Activity native values

The primary level signifies that any duties that we create inside an actor will take part in actor isolation for that particular actor. For instance, we will safely entry an actor’s strategies and properties from inside a activity that’s created inside an actor:

actor SampleActor {
  var someCounter = 0

  func incrementCounter() {
    Activity {
      someCounter += 1
    }
  }
}

If we have been to mutate someCounter from a context that isn’t operating on this particular actor we’d should prefix our someCounter += 1 line with an await since we would have to attend for the actor to be out there.

This isn’t the case for an unstructured activity that we’ve created from inside an actor.

Notice that our activity doesn’t have to finish earlier than the incrementCounter() methodology returns. That reveals us that the unstructured activity that we created isn’t taking part in structured concurrency. If it have been, incrementCounter() wouldn’t be capable to full earlier than our activity accomplished.

Equally, if we spawn a brand new unstructured activity from a context that’s annotated with @MainActor, the duty will run its physique on the principle actor:

@MainActor
func fetchData() {
  Activity {
    // this activity runs its physique on the principle actor
    let knowledge = await fetcher.getData()

    // self.fashions is up to date on the principle actor
    self.fashions = knowledge
  }
}

It’s vital to notice that the await fetcher.getData() line does not block the principle actor. We’re calling getData() from a context that’s operating on the principle actor however that doesn’t imply that getData() itself will run its physique on the principle actor. Except getData() is explicitly related to the principle actor it is going to at all times run on a background thread.

Nonetheless, the duty does run its physique on the principle actor so as soon as we’re not ready for the results of getData(), our activity resumes and self.fashions is up to date on the principle actor.

Notice that whereas we await one thing, our activity is suspended which permits the principle actor to do different work whereas we wait. We don’t block the principle actor by having an await on it. It’s actually fairly the other.

When to make use of unstructured duties

You’ll mostly create unstructured duties if you wish to name an async annotated perform from a spot in your code that isn’t but async. For instance, you may wish to fetch some knowledge in a viewDidLoad methodology, otherwise you may wish to begin iterating over a few async sequences from inside a single place.

Another excuse to create an unstructured activity could be if you wish to carry out a bit of labor independently of the perform you’re in. This might be helpful if you’re implementing a fire-and-forget type logging perform for instance. The log may must be despatched off to a server, however as a caller of the log perform I’m not focused on ready for that operation to finish.

func log(_ string: String) {
  print("LOG", string)
  Activity {
    await uploadMessage(string)
    print("message uploaded")
  }
}

We may have made the strategy above async however then we wouldn’t be capable to return from that methodology till the log message was uploaded. By placing the add in its personal unstructured activity we permit log(_:) to return whereas the add remains to be ongoing.

Creating indifferent duties with Activity.indifferent

Indifferent duties are in some ways just like unstructured duties. They don’t create a mother or father / baby relationship, they don’t take part in structured concurrency and so they create a model new island of concurrency that we will work with.

The important thing distinction is {that a} indifferent activity won’t inherit something from the context that it was created in. Which means that a indifferent activity won’t inherit the present actor, and it’ll not inherit activity native values.

Think about the instance you noticed earlier:

actor SampleActor {
  var someCounter = 0

  func incrementCounter() {
    Activity {
      someCounter += 1
    }
  }
}

As a result of we used a unstructed activity on this instance, we have been capable of work together with our actor’s mutable state with out awaiting it.

Now let’s see what occurs once we make a indifferent activity as an alternative:

actor SampleActor {
  var someCounter = 0

  func incrementCounter() {
    Activity.indifferent {
      // Actor-isolated property 'someCounter' can't be mutated from a Sendable closure
      // Reference to property 'someCounter' in closure requires specific use of 'self' to make seize semantics specific
      someCounter += 1
    }
  }
}

The compiler now sees that we’re not on the SampleActor inside our indifferent activity. Which means that now we have to work together with the actor by calling its strategies and properties with an await.

Equally, if we create a indifferent activity from an @MainActor annotated methodology, the indifferent activity won’t run its physique on the principle actor:

@MainActor
func fetchData() {
  Activity.indifferent {
    // this activity runs its physique on a background thread
    let knowledge = await fetcher.getData()

    // self.fashions is up to date on a background thread
    self.fashions = knowledge
  }
}

Notice that detaching our activity has no impression in any respect on the place getData() executed. Since getData() is an async perform it is going to at all times run on a background thread until the strategy was explicitly annotated with an @MainActor annotation. That is true no matter which actor or thread we name getData() from. It’s not the callsite that decides the place a perform runs. It’s the perform itself.

When to make use of indifferent duties

Utilizing a indifferent activity solely is sensible if you’re performing work inside the duty physique that you simply wish to run away from any actors it doesn’t matter what. If you happen to’re awaiting one thing inside the indifferent activity to ensure the awaited factor runs within the background, a indifferent activity isn’t the software you have to be utilizing.

Even in case you solely have a sluggish for loop inside a indifferent activity, or your encoding a considerable amount of JSON, it would make extra sense to place that work in an async perform so you will get the advantages of structured concurrency (the work should full earlier than we will return from the calling perform) in addition to the advantages of operating within the background (async capabilities run within the background by default).

So a indifferent activity actually solely is sensible if the work you’re doing ought to be away from the principle thread, doesn’t contain awaiting a bunch of capabilities, and the work you’re doing mustn’t take part in structured concurrency.

As a rule of thumb I keep away from indifferent duties till I discover that I really want one. Which is just very sporadically.

In Abstract

On this submit you realized in regards to the variations between indifferent duties and unstructured duties. You realized that unstructured duties inherit context whereas indifferent duties don’t. You additionally realized that neither a indifferent activity nor an unstructured activity turns into a toddler activity of their context as a result of they don’t take part in structured concurrency.

You realized that unstructured duties are the popular technique to create new duties. You noticed how unstructured duties inherit the actor they’re created from, and also you realized that awaiting one thing from inside a activity doesn’t be certain that the awaited factor runs on the identical actor as your activity.

After that, you realized how indifferent duties are unstructured, however they don’t inherit any context from when they’re created. In observe because of this they at all times run their our bodies within the background. Nonetheless, this doesn’t be certain that awaited capabilities additionally run within the background. An @MainActor annotated perform will at all times run on the principle actor, and any async methodology that’s not constrained to the principle actor will run within the background. This conduct makes indifferent duties a software that ought to solely be used when no different software solves the issue you’re fixing.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles