iosswiftasync-awaitactor

Understanding GlobalActor, is it guaranteed it won't execute on Main Thread


I'm learning usages of async-await and along with that read about the Global Actors. I know MainActor is GlobalActor, and if we annotate a method with @mainActor, the method will be executed on Main Thread. Similar to that if we create a custom GlobalActor, do we know which thread it will use to perform.

@globalActor actor CustomGlobalActor {
     static let shared = CustomGlobalActor()
 }

@CustomGlobalActor func performTask() async -> Output {
    .....
}

Also, additional questions:

  1. Can we use it interchangeably to perform time consuming tasks in background thread?
  2. Is it connected to Global Queues in any manner?

Solution

  • A few observations:

    1. A “global actor” (as defined in SE-0316) is just a named actor that is accessible globally. If you define your own global actor, you can access that particular actor by that name globally throughout your app.

      I know you know this, but the main actor is a global actor, too. It is just a special, predefined, one that happens to rely upon the main thread. (It is analogous to the fact that GCD’s “main queue” is just a special dispatch queue that happens to use the main thread.)

      But, note, when one starts worrying about what thread a particular task on an particular actor is using, it is irrelevant whether that actor is a global actor or not. We’re only concerned about actor behavior at that point. For the purposes of this conversation, we don’t particularly care if the actor is a global actor or not. Just that it is an actor.

    2. Probably needless to say at this point, the “global” of a “global actor” has nothing to do with GCD “global” dispatch queues. They are completely different concepts.

    3. You say:

      Similar to that if we create a custom GlobalActor, do we know which thread it will use to perform.

      This is splitting hairs, but, no, other than the main actor (which is tied to the main thread), actors in general are not tied to a particular thread. (Swift concurrency strives to abstract us await from thread-level details, anyway.)

      Just as one should avoid conflating threads and GCD dispatch queues, the same is true with actors. If you have three tasks that you have submitted to some non-main actor, you have no assurances that they will run on the same thread. You only know that whatever thread they do run on, the different tasks on that actor will not actively run at the same as the other tasks on that same actor. Because the tasks on a particular actor will not run in parallel, they may well end up running on the same thread, one after another. But they may not.

      If you are interested in these details, see WWDC 2021 Swift concurrency: Behind the scenes.

    4. Yes, in practice, a task running on a non-main actor (whether it happens to be a global actor or not) can be used to get a time-consuming synchronous task (such as some computationally-intensive process) off the main thread.

      But that is not the primary purpose of actors. They’re more for allowing us to manage some mutable shared state across different tasks. See WWDC 2021 Protect mutable state with Swift actors.

      So, if you have a computationally intense calculation that you want to get off the main thread, yes, you can theoretically use an actor. And if you want that actor to be a global actor, you can do that, too (though that is less common). But if all you want to do is to get some computationally intense process off the main thread, you might just use an async non-isolated function (see SE-0338) or a detached task, and call it a day. Actors are more about managing state across tasks than getting something off the main thread.

    5. You ask:

      Can we use it interchangeably to perform time consuming tasks in background thread?

      One should be wary about time consuming tasks that do not contain any suspension points within async-await. Swift concurrency imposes a contract to never impede forward progress. If you have some slow, synchronous task that you want to run within Swift concurrency, a well-behaved routine would periodically Task.yield. See https://stackoverflow.com/a/76665311/1271826.