HomeiOS DevelopmentUnderstanding unstructured and indifferent duties in Swift – Donny Wals

Understanding unstructured and indifferent duties in Swift – Donny Wals


Revealed on: April 13, 2023

Swift Concurrency closely makes use of duties to mannequin chunks of asynchronous work. In reality, the place GCD depends on dispatch queues that execute work objects you possibly can say that Swift Concurrency depends on duties that get run by threads.

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 mum or dad / baby relationship between duties, one other creates duties which might be unstructured however do inherit some context and there’s an strategy that creates duties which might be utterly indifferent from all context.

On this put up, I’ll deal with unstructured and indifferent duties. In the event you’re interested by studying extra about baby duties, I extremely suggest that you just learn the next posts:

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

This put up assumes that you just perceive the fundamentals of structured concurrency which you’ll be taught extra about in this put up. You don’t need to have mastered the subject of structured concurrency, however having some sense of what structured concurrency is all about will enable you perceive this put up a lot better.

Creating unstructured duties with Activity.init

The most typical means during which you’ll be creating duties in Swift will likely be with Activity.init which you’ll in all probability write as follows with out spelling out the .init:

Activity {
  // carry out work
}

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

Nonetheless, that doesn’t make an unstructured activity is created solely impartial from every little thing else.

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

  • The actor we’re at the moment working 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 are able to 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 had been to mutate someCounter from a context that isn’t working on this particular actor we’d need to prefix our someCounter += 1 line with an await since we’d have to attend for the actor to be obtainable.

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

Word that our activity doesn’t have to finish earlier than the incrementCounter() technique returns. That reveals us that the unstructured activity that we created isn’t collaborating in structured concurrency. If it had been, incrementCounter() wouldn’t have the ability 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 primary actor:

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

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

It’s vital to notice that the await fetcher.getData() line does not block the primary actor. We’re calling getData() from a context that’s working on the primary actor however that doesn’t imply that getData() itself will run its physique on the primary actor. Until getData() is explicitly related to the primary actor it can all the time run on a background thread.

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

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

When to make use of unstructured duties

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

One more reason to create an unstructured activity is perhaps if you wish to carry out a chunk of labor independently of the perform you’re in. This might be helpful if you’re implementing a fire-and-forget fashion logging perform for instance. The log would possibly should be despatched off to a server, however as a caller of the log perform I’m not interested by ready for that operation to finish.

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

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

Creating indifferent duties with Activity.indifferent

Indifferent duties are in some ways much like unstructured duties. They don’t create a mum or dad / baby relationship, they don’t take part in structured concurrency they usually create a model new island of concurrency that we are able to work with.

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

Contemplate the instance you noticed earlier:

actor SampleActor {
  var someCounter = 0

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

As a result of we used a indifferent activity, we had been in a position to 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 a substitute:

actor SampleActor {
  var someCounter = 0

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

The compiler now sees that we’re now not on the SampleActor inside our indifferent activity. Which means that we have now 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 technique, the indifferent activity is not going to run its physique on the primary 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
  }
}

Word that detaching our activity has no affect in any respect on the place getData() executed. Since getData() is an async perform it can all the time 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 just need to run away from any actors it doesn’t matter what. In the event you’re awaiting one thing inside the indifferent activity to verify the awaited factor runs within the background, a indifferent activity isn’t the device try to be utilizing.

Even when you solely have a gradual 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 may get the advantages of structured concurrency (the work should full earlier than we are able to return from the calling perform) in addition to the advantages of working 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 must be away from the primary thread, doesn’t contain awaiting a bunch of capabilities, and the work you’re doing shouldn’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 barely very sporadically.

In Abstract

On this put up you discovered in regards to the variations between indifferent duties and unstructured duties. You discovered that unstructured duties inherit context whereas indifferent duties don’t. You additionally discovered 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 discovered that unstructured duties are the popular method to create new duties. You noticed how unstructured duties inherit the actor they’re created from, and also you discovered that awaiting one thing from inside a activity doesn’t be sure that the awaited factor runs on the identical actor as your activity.

After that, you discovered how indifferent duties are unstructured, however they don’t inherit any context from when they’re created. In follow which means they all the time run their our bodies within the background. Nonetheless, this doesn’t be sure that awaited capabilities additionally run within the background. An @MainActor annotated perform will all the time run on the primary actor, and any async technique that’s not constrained to the primary actor will run within the background. This conduct makes indifferent duties a device that ought to solely be used when no different device solves the issue you’re fixing.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments