Exploring concurrency modifications in Swift 6.2 – Donny Wals

0
1
Exploring concurrency modifications in Swift 6.2 – Donny Wals


It is no secret that Swift concurrency could be fairly troublesome to be taught. There are plenty of ideas which are totally different from what you are used to if you had been writing code in GCD. Apple acknowledged this in considered one of their imaginative and prescient paperwork they usually got down to make modifications to how concurrency works in Swift 6.2. They don’t seem to be going to vary the basics of how issues work. What they may primarily change is the place code will run by default.

On this weblog put up, I would really like to check out the 2 primary options that may change how your Swift concurrency code works:

  1. The brand new nonisolated(nonsending) default function flag
  2. Working code on the principle actor by default with the defaultIsolation setting

By the tip of this put up you need to have a fairly good sense of the influence that Swift 6.2 can have in your code, and the way try to be transferring ahead till Swift 6.2 is formally obtainable in a future Xcode launch.

Understanding nonisolated(nonsending)

The nonisolated(nonsending) function is launched by SE-0461 and it’s a fairly large overhaul when it comes to how your code will work transferring ahead. On the time of scripting this, it’s gated behind an upcoming function compiler flag referred to as NonisolatedNonsendingByDefault. To allow this flag in your venture, see this put up on leveraging upcoming options in an SPM package deal, or in the event you’re trying to allow the function in Xcode, check out enabling upcoming options in Xcode.

For this put up, I’m utilizing an SPM package deal so my Bundle.swift accommodates the next:

.executableTarget(
    identify: "SwiftChanges",
    swiftSettings: [
        .enableExperimentalFeature("NonisolatedNonsendingByDefault")
    ]
)

I’m getting forward of myself although; let’s speak about what nonisolated(nonsending) is, what downside it solves, and the way it will change the best way your code runs considerably.

Exploring the issue with nonisolated in Swift 6.1 and earlier

Whenever you write async features in Swift 6.1 and earlier, you may achieve this on a category or struct as follows:

class NetworkingClient {
  func loadUserPhotos() async throws -> [Photo] {
    // ...
  }
}

When loadUserPhotos is named, we all know that it’s going to not run on any actor. Or, in additional sensible phrases, we all know it’ll run away from the principle thread. The explanation for that is that loadUserPhotos is a nonisolated and async perform.

Which means when you have got code as follows, the compiler will complain about sending a non-sendable occasion of NetworkingClient throughout actor boundaries:

struct SomeView: View {
  let community = NetworkingClient()

  var physique: some View {
    Textual content("Howdy, world")
      .process { await getData() }
  }

  func getData() async {
    do {
      // sending 'self.community' dangers inflicting knowledge races
      let images = attempt await community.loadUserPhotos()
    } catch {
      // ...
    }
  }
}

Whenever you take a more in-depth have a look at the error, the compiler will clarify:

sending primary actor-isolated ‘self.community’ to nonisolated occasion methodology ‘loadUserPhotos()’ dangers inflicting knowledge races between nonisolated and primary actor-isolated makes use of

This error is similar to one that you just’d get when sending a primary actor remoted worth right into a sendable closure.

The issue with this code is that loadUserPhotos runs in its personal isolation context. Which means it would run concurrently with no matter the principle actor is doing.

Since our occasion of NetworkingClient is created and owned by the principle actor we are able to entry and mutate our networking occasion whereas loadUserPhotos is operating in its personal isolation context. Since that perform has entry to self, it signifies that we are able to have two isolation contexts entry the identical occasion of NetworkingClient at the very same time.

And as we all know, a number of isolation contexts accessing the identical object can result in knowledge races if the article isn’t sendable.

The distinction between an async and non-async perform that’s nonisolated like loadUserPhotos is that the non-async perform would run on the caller’s actor. So if we name a nonisolated async perform from the principle actor then the perform will run on the principle actor. After we name a nonisolated async perform from a spot that’s not on the principle actor, then the referred to as perform will not run on the principle actor.

Swift 6.2 goals to repair this with a brand new default for nonisolated features.

Understanding nonisolated(nonsending)

The conduct in Swift 6.1 and earlier is inconsistent and complicated for folk, so in Swift 6.2, async features will undertake a brand new default for nonisolated features referred to as nonisolated(nonsending). You don’t have to write down this manually; it’s the default so each nonisolated async perform shall be nonsending except you specify in any other case.

When a perform is nonisolated(nonsending) it signifies that the perform gained’t cross actor boundaries. Or, in a extra sensible sense, a nonisolated(nonsending) perform will run on the caller’s actor.

So after we opt-in to this function by enabling the NonisolatedNonsendingByDefault upcoming function, the code we wrote earlier is totally nice.

The explanation for that’s that loadUserPhotos() would now be nonisolated(nonsending) by default, and it might run its perform physique on the principle actor as a substitute of operating it on the cooperative thread pool.

Let’s check out some examples, we could? We noticed the next instance earlier:

class NetworkingClient {
  func loadUserPhotos() async throws -> [Photo] {
    // ...
  }
}

On this case, loadUserPhotos is each nonisolated and async. Which means the perform will obtain a nonisolated(nonsending) therapy by default, and it runs on the caller’s actor (if any). In different phrases, in the event you name this perform on the principle actor it would run on the principle actor. Name it from a spot that’s not remoted to an actor; it would run away from the principle thread.

Alternatively, we’d have added a @MainActor declaration to NetworkingClient:

@MainActor
class NetworkingClient {
  func loadUserPhotos() async throws -> [Photo] {
    return [Photo()]
  }
}

This makes loadUserPhotos remoted to the principle actor so it would at all times run on the principle actor, irrespective of the place it’s referred to as from.

Then we’d even have the principle actor annotation together with nonisolated on loadUserPhotos:

@MainActor
class NetworkingClient {
  nonisolated func loadUserPhotos() async throws -> [Photo] {
    return [Photo()]
  }
}

On this case, the brand new default kicks in regardless that we didn’t write nonisolated(nonsending) ourselves. So, NetworkingClient is primary actor remoted however loadUserPhotos isn’t. It can inherit the caller’s actor. So, as soon as once more if we name loadUserPhotos from the principle actor, that’s the place we’ll run. If we name it from another place, it would run there.

So what if we wish to make it possible for our perform by no means runs on the principle actor? As a result of to this point, we’ve solely seen prospects that might both isolate loadUserPhotos to the principle actor, or choices that might inherit the callers actor.

Working code away from any actors with @concurrent

Alongside nonisolated(nonsending), Swift 6.2 introduces the @concurrent key phrase. This key phrase will will let you write features that behave in the identical means that your code in Swift 6.1 would have behaved:

@MainActor
class NetworkingClient {
  @concurrent
  nonisolated func loadUserPhotos() async throws -> [Photo] {
    return [Photo()]
  }
}

By marking our perform as @concurrent, we make it possible for we at all times depart the caller’s actor and create our personal isolation context.

The @concurrent attribute ought to solely be utilized to features which are nonisolated. So for instance, including it to a technique on an actor gained’t work except the tactic is nonisolated:

actor SomeGenerator {
  // not allowed
  @concurrent
  func randomID() async throws -> UUID {
    return UUID()
  }

  // allowed
  @concurrent
  nonisolated func randomID() async throws -> UUID {
    return UUID()
  }
}

Observe that on the time of writing each circumstances are allowed, and the @concurrent perform that’s not nonisolated acts prefer it’s not remoted at runtime. I count on that it is a bug within the Swift 6.2 toolchain and that this may change for the reason that proposal is fairly clear about this.

How and when must you use NonisolatedNonSendingByDefault

For my part, opting in to this upcoming function is a good suggestion. It does open you as much as a brand new means of working the place your nonisolated async features inherit the caller’s actor as a substitute of at all times operating in their very own isolation context, nevertheless it does make for fewer compiler errors in apply, and it truly helps you eliminate an entire bunch of primary actor annotation primarily based on what I’ve been capable of attempt to this point.

I’m an enormous fan of decreasing the quantity of concurrency in my apps and solely introducing it once I wish to explicitly achieve this. Adopting this function helps rather a lot with that. Earlier than you go and mark every little thing in your app as @concurrent simply to make certain; ask your self whether or not you actually should. There’s most likely no want, and never operating every little thing concurrently makes your code, and its execution rather a lot simpler to motive about within the large image.

That’s very true if you additionally undertake Swift 6.2’s second main function: defaultIsolation.

Exploring Swift 6.2’s defaultIsolation choices

In Swift 6.1 your code solely runs on the principle actor if you inform it to. This might be because of a protocol being @MainActor annotated otherwise you explicitly marking your views, view fashions, and different objects as @MainActor.

Marking one thing as @MainActor is a fairly widespread resolution for fixing compiler errors and it’s most of the time the suitable factor to do.

Your code actually doesn’t have to do every little thing asynchronously on a background thread.

Doing so is comparatively costly, usually doesn’t enhance efficiency, and it makes your code rather a lot tougher to motive about. You wouldn’t have written DispatchQueue.international() all over the place earlier than you adopted Swift Concurrency, proper? So why do the equal now?

Anyway, in Swift 6.2 we are able to make operating on the principle actor the default on a package deal stage. It is a function launched by SE-0466.

This implies that you may have UI packages and app targets and mannequin packages and many others, mechanically run code on the principle actor except you explicitly opt-out of operating on primary with @concurrent or by your individual actors.

Allow this function by setting defaultIsolation in your swiftSettings or by passing it as a compiler argument:

swiftSettings: [
    .defaultIsolation(MainActor.self),
    .enableExperimentalFeature("NonisolatedNonsendingByDefault")
]

You don’t have to make use of defaultIsolation alongside NonisolatedNonsendingByDefault however I did like to make use of each choices in my experiments.

At present you may both move MainActor.self as your default isolation to run every little thing on primary by default, or you should use nil to maintain the present conduct (or don’t move the setting in any respect to maintain the present conduct).

When you allow this function, Swift will infer each object to have an @MainActor annotation except you explicitly specify one thing else:

@Observable
class Individual {
  var myValue: Int = 0
  let obj = TestClass()

  // This perform will _always_ run on primary 
  // if defaultIsolation is about to primary actor
  func runMeSomewhere() async {
    MainActor.assertIsolated()
    // do some work, name async features and many others
  }
}

This code accommodates a nonisolated async perform. Which means, by default, it might inherit the actor that we name runMeSomewhere from. If we name it from the principle actor that’s the place it runs. If we name it from one other actor or from no actor, it runs away from the principle actor.

This most likely wasn’t supposed in any respect.

Possibly we simply wrote an async perform in order that we might name different features that wanted to be awaited. If runMeSomewhere doesn’t do any heavy processing, we most likely need Individual to be on the principle actor. It’s an observable class so it most likely drives our UI which signifies that just about all entry to this object ought to be on the principle actor anyway.

With defaultIsolation set to MainActor.self, our Individual will get an implicit @MainActor annotation so our Individual runs all its work on the principle actor.

Let’s say we wish to add a perform to Individual that’s not going to run on the principle actor. We are able to use nonisolated similar to we might in any other case:

// This perform will run on the caller's actor
nonisolated func runMeSomewhere() async {
  MainActor.assertIsolated()
  // do some work, name async features and many others
}

And if we wish to make sure that we’re by no means on the principle actor:

// This perform will run on the caller's actor
@concurrent
nonisolated func runMeSomewhere() async {
  MainActor.assertIsolated()
  // do some work, name async features and many others
}

We have to opt-out of this primary actor inference for each perform or property that we wish to make nonisolated; we are able to’t do that for all the sort.

After all, your individual actors won’t all of the sudden begin operating on the principle actor and kinds that you just’ve annotated with your individual international actors aren’t impacted by this modification both.

Do you have to opt-in to defaultIsolation?

It is a powerful query to reply. My preliminary thought is “sure”. For app targets, UI packages, and packages that primarily maintain view fashions I positively assume that going primary actor by default is the suitable alternative.

You possibly can nonetheless introduce concurrency the place wanted and it will likely be rather more intentional than it might have been in any other case.

The truth that whole objects shall be made primary actor by default looks like one thing that may trigger friction down the road however I really feel like including devoted async packages could be the best way to go right here.

The motivation for this selection current makes plenty of sense to me and I feel I’ll wish to attempt it out for a bit earlier than making up my thoughts absolutely.

LEAVE A REPLY

Please enter your comment!
Please enter your name here