© Andrés Ibañez Kautsch 2023
A. I. KautschModern Concurrency on Apple Platformshttps://doi.org/10.1007/978-1-4842-8695-1_8

8. The Main Actor & Final Actors

Andrés Ibañez Kautsch1  
(1)
La Paz, Bolivia
 

In Chapter 6, we learned about actors. Actors are reference types that synchronize their own internal state, so it is safe to mutate them from different threads. This is done by guaranteeing that only one task or thread can mutate the actor’s state at a time. Actors are sendable types meant to be used in a concurrent context. Actors are types that you instantiate and, for the most part, treat as normal classes. But what happens when we need something that is always guaranteed to run on the same thread, but when it may be spread out, even across different files?

Before we study global actors, I want to talk about an old friend of ours: the main thread. I want to do this now because it is linked to this chapter’s main topic.

The Main Thread

If you have been programming for Apple platforms for a while, you know about The Main Thread . If this is the first time you hear about it, worry not, because we are going to have a refresher on it in this section. The main thread’s sole responsibility is to run your UI code. If you want to update a label, add some color to view, or toggle a switch, you need to do those operations on the main thread. We are not allowed to update our UI outside the main thread. Doing so will result in warnings in the best case and runtime crashes in the worst case. If we are running asynchronous tasks – whether with the new concurrency system or with older tools such as the GCD – and you are interested in showing the returning data in your UI, you need to do that on the main thread.

Because the main thread is responsible of updating your UI , doing any expensive operations on it will leave your app hanged for a few seconds, or it can leave your app with visible stutters and visible frame drops in the UI. If your app hangs for a long time, then the system will take matters in its own hands and kill it. This is the reason a lot of APIs, including all the networking calls in URLSession, default to running in separate threads, doesn’t matter if you use the async/await variations of these methods or the traditional closure-based ones. You can force URLSession and other asynchronous APIs to run on the main thread, but you absolutely shouldn’t, and I doubt you will ever find a good excuse for it.

In short, anything you do with UIKit (and by extension, SwiftUI) needs to be done on the main thread, and you need to avoid clogging it up with expensive work. Traditionally, we would handle data to the main thread calling something like DispatchQueue.main.async. This method takes a closure that executes on the main thread so it’s ideal to work with your UI there. DispatchQueue is an object from the GCD, and the main static property refers to the main queue, which always runs on the main thread. This is a quick and painless way to get back to the main thread, safe, but it is also a fact that it can create pretty pyramids of doom if you are using it extensively. Any object that starts with the “UI” prefix (UIButton, UISwitch) in iOS or any AppKit object in macOS should be mutated from the main thread only.

Tip

As of Xcode 14 and all OS releases accompanying it, UIImage and UIColor are the exception to the rule that states “all UI-prefixed objects should be mutated from the main thread.” Starting on this Xcode version, UIImage and UIColor conform to the Sendable protocol , so it is safe to pass them across different concurrency domains.

In the case of the modern concurrency system , the system will decide if it should launch your tasks in different threads. Remember we learned that our tasks suspend when they hit an await keyword and the system decided to launch that call in a different thread. If a suspension takes place, you do not know where the code will execute, or even what thread it will use to deliver its results. How can we deliver results to the main thread when using the new concurrency system, so we can update our UI after finishing an expensive task? Well, I’m glad you asked!

The Main Actor

I can finally explain to you something that we have been using throughout the book, but it was hard to find an appropriate time to explain it. Yes, I am talking about the main actor, which in code appears as @MainActor.

UIKit and SwiftUI are both big frameworks. If you think about it, they are made up of hundreds if not thousands of classes and they are likely spread out in different files within the framework. They both have the requirement that any UI update should be done on the main thread. Marking a declaration as @MainActor means that that specific piece of functionality will run on the main actor. “@MainActor” is essentially treated as an attribute. As iOS 15, macOS 12, watchOS 8, and tvOS 16, most of the classes in these frameworks are marked with @MainActor. In the case of SwiftUI, creating ObservableObjects should manually be marked as @MainActor as the protocol doesn’t come with it out of the box. Marking all objects in UIKit, SwiftUI, and AppKit as @MainActor is a very elegant solution to making sure all of them run on the main thread.

Using the @MainActor

Just like the @Sendable attributes , you can use the @MainActor “attribute ” in many different places as well. First, you can use it as part of a type declaration, like a struct or a class. Listing 8-1 declares two new types, one of them is marked as @MainActor.
struct MusicAlbum {
    let name: String
    let artist: String
    let releaseYear: Int
}
@MainActor class MusicLibrary {
    var name: String
    var albums: [MusicAlbum] = []
    init(name: String) {
        self.name = name
    }
}
Listing 8-1

Adding @MainActor to a class

Because of this, every operation we want to perform on MusicLibrary instances needs to run on the main actor as well. Every property and method within MusicLibrary will run on the main actor as well. Imagine you have a function, createLibraryAndAddAlbum, that is created outside the context of the MusicLibrary object itself. You may be tempted to work on your music library within this new function. In Listing 8-2, we attempt to use this type in a context that does not run on the @MainActor.
func createLibraryAndAddAlbum() {
    let library = MusicLibrary(name: "Andy's Library")
    let album = MusicAlbum(name: "Imaginareum", artist: "Nightwish", releaseYear: 2011)
    library.shows += [album]
}
Listing 8-2

Working on a MusicLibrary without being on a Main Actor context

If you try to compile this, you will get errors that are very similar to what you would get when attempting to send non-sendable types across different concurrency domains. Namely:
Call to main actor-isolated initializer 'init(name:)' in a synchronous nonisolated context
...
Property 'albums' isolated to global actor 'MainActor' can not be mutated from this context

By adding @MainActor to createLibraryAndAddAlbum, the function will be running on main actor too. Both the MusicLibrary and createLibraryAndAddAlbum function will be running on the same actor (the same thread), and it doesn’t matter if these two are in the same file or even across different frameworks. This is the reason the main actor is called a global actor.

Listing 8-3 will compile without an issue.
@MainActor
func createLibraryAndAddAlbum() {
    let library = MusicLibrary(name: "Andy's Library")
    let album = MusicAlbum(name: "Imaginareum", artist: "Nightwish", releaseYear: 2011)
    library.albums += [album]
}
Listing 8-3

We can add @MainActor to declarations that are spread out, even in different files or frameworks

If you are in a different actor, you can call methods marked as @MainActor, but doing so requires you to do it asynchronously (using await or async let), because the @MainActor synchronizes its own internal state across all declarations that use it.

Now, this is where a lot of people get confused when working with the main actor.

Suppose you add the method in Listing 8-4 to the MusicLibrary object.
@MainActor
func populateFromWebService() async {
    albums = []
    albums = try await WebService.shared.albumsFromService()
}
Listing 8-4

An asynchronous method to fetch albums

Assume that the populateFromWebService method is a long-running operation that will suspend. Where will the actor execute?

The long-running operation, the populateFromWebService function itself, will still run on a different thread. You do not know where it will run. You just know that your method on the main actor will suspend, and the thread will be doing other work before coming back to you.

What will execute on the main actor itself is the assignment of the albums methods. Just the assignment. populateFromWebService will be running somewhere else, but the assignment of its returned data to self.albums will run on the main actor. A lot of people are under the impression that populateFromWebService itself would execute on the main actor. That is, if this method is downloading some JSON data from a slow service and parsing it or doing anything else that can take anything longer than milliseconds, it is running on the main thread. That is not correct, and fortunately the global actors system is smarter than that. If you are running something on @MainActor, always know that all its statements will run on the main actor, but if it finds a call that suspends, the execution of whatever triggered the suspension will be done somewhere else, and the data will be returned (and assigned) on the main thread. The “left” side of an await will execute on the main thread, and the “right” side of the await keyword will take place someplace else, so to speak.

You do not have to mark an entire class or struct declaration as main actor. If you only require some properties and methods to run on the main actor, you can mark only those as @MainActor. Listing 8-5 modifies the MusicLibrary object to add a new variable and a new method, gets rid of the @MainActor mark in the entire declaration, and adds it to the newly added symbol.
class MusicLibrary {
    var name: String
    var albums: [MusicAlbum] = []
    @MainActor var albumCover: UIImage?
    init(name: String) {
        self.name = name
    }
    @MainActor func updateAlbumCover() {
        // ... Download a new cover from the internet
        // and update the albumCover property.
    }
}
Listing 8-5

Marking only some properties and methods as @MainActor

Now this class will be usable everywhere, but whatever action we want to do on the albumCover, it must be done in a context that is running on the main actor.

If you isolate an entire class to run on the main actor (by marking it as @MainActor class MyClass…), then that class will always run on the main actor and calling it from somewhere else will need to await. If you have some methods that need to be nonisolated to the main actor (as a normal actor definition, global actors can have methods that are nonisolated), you can mark it as nonisolated and use it from any other concurrent context.

Listing 8-6 marks the MusicLibrary class as running on the main actor and adds a nonisolated method to it.
@MainActor
class MusicLibrary {
    var name: String
    var albums: [MusicAlbum] = []
    @MainActor var albumCover: UIImage?
    init(name: String) {
        self.name = name
    }
    nonisolated func libraryPurpose() -> String {
        "This library holds my favorite songs"
    }
}
Listing 8-6

Adding a nonisolated method to a class running on the main actor

Note that nonisolated methods need to be truly isolated. You cannot return a property derived from one of our properties in the class. We would not be able to return the name, the albums, or even the number of albums (albums.count), because those properties are isolated to the main actor, and we cannot return isolated properties through a nonisolated context. It is also not possible to use the nonisolated keyword with stored properties.

Just like @Sendable, we can also mark closures and functions with @MainActor to run them on the main thread. We have seen how a function can be marked as @MainActor. For closures, you can use a syntax similar to @Sendable, but by replacing @Sendable with @MainActor. In general, wherever you can use @Sendable, you can also use @MainActor. This includes a variable closure declaration, a closure declaration as a parameter to a function, and more.

Listing 8-7 shows how to mark a closure as @MainActor.
Task { @MainActor in
}
Listing 8-7

Marking a closure as running on the @MainActor

In this particular example, we are declaring a Task that will run on the main actor. I wanted to show you this so you can see that you can really use @MainActor everywhere. It may not be a good idea to run a closure on the main thread unless it is to update your UI.

You can also run any code on the @MainActor by using a static run method. Listing 8-8 shows how this is done.
Task {
     await MainActor.run {
          // Update your UI.
     }
}
Listing 8-8

Running code on the main actor

The await keyword is necessary. Because we may not be in a context that is also running on the main actor, the calls need to be done asynchronously. This method of running code on the main actor is useful. If you have to keep writing closure-based code and you can’t migrate to async/await completely just yet, you can use this to quickly relay information back to the main thread without having to use the GCD and its DispatchQueue.main.async method.

Understanding Global Actors

To understand what a global actor is, the easiest way to do so is by understanding the main actor.

As we said before, the main actor is a global actor. A global actor can be applied to various declarations, even if they are across different files in a framework. Earlier, we mentioned that UIKit, the main framework to make iOS apps other than SwiftUI, and its equivalent macOS counterpart, AppKit, have their types spread out across different files. There had to be a way to ensure these UI classes ran on the main thread in a way that played well with the new Swift concurrency system.

An actor that can be applied across different declarations, even if they are in different files, is a global actor. As you have seen throughout this chapter, the main actor is a very important part of developing modern concurrent apps on Apple platforms, because it provides us with a way to do work on the main thread to update our UI. The main actor will synchronize all access to it to ensure all UI updates are done properly, with no race conditions.

@MainActor is a global actor Apple provides to us. It’s great, it does its work, but can we create our own global actors? The answer is a loud yes!

Creating Global Actors

You can create your own global actors to isolate them from the rest of the program, similar to how the @MainActor isolates its state to the main thread for UI updates. To do so, declare a struct that will be used as an attribute, and give it a static property named shared. Mark this struct with the @globalActor attribute as well.

Listing 8-9 declares a global actor called @MediaActor .
@globalActor
struct MediaActor {
    actor ActorType { }
    static let shared: ActorType = ActorType()
}
Listing 8-9

Declaring our own global actor

Now everything we mark as @MediaActor will run on the same actor, synchronizing the global actor’s state automatically and ensuring no data races take place. In this example, we named it MediaActor , but you can name your actors anything that makes sense in the context of your app, such as @DatabaseActor .

In the example in Listing 8-10, we will create a global Videogames array. We will then read and write it from multiple different places to show our @MediaActor in action.
struct Videogame {
    let id = UUID()
    let name: String
    let releaseYear: Int
    let developer: String
}
@MediaActor var videogames: [Videogame] = []
Listing 8-10

Creating a new object and a global variable that runs on our MediaActor

Note

in this example, videogames is a global variable. In general, I do not condone the usage of global variables like this, but I think it’s a very good example to learn about global actors.

Now, suppose you have a view controller where you can perform operations on this array. We can add a random videogame, remove a random videogame, and retrieve the information of a random videogame . Listing 8-11 shows a short version of such a view controller.
@MainActor
class ViewController: UIViewController {
    @MediaActor
    func addRandomVideogames() {
        let zeldaOot = Videogame(name: "The Legend of Zelda: Ocarina of Time", releaseYear: 1998, developer: "Nintendo")
        let xillia = Videogame(name: "Tales of Xillia", releaseYear: 2013, developer: "Bandai Namco")
        let legendOfHeroes = Videogame(name: "The Legend of Heroes: A Tear of Vermilion", releaseYear: 2004, developer: "Nihon Falcom")
        videogames += [zeldaOot, xillia, legendOfHeroes]
    }
    @MediaActor
    func removeRandomvideogame() {
        if let randomElement = videogames.randomElement() {
            videogames.removeAll { $0.id == randomElement.id }
        }
    }
    @MediaActor
    func getRandomGame() -> Videogame? {
        return videogames.randomElement()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        Task {
            await addRandomVideogames()
            await removeRandomvideogame()
            if let randomGame = await getRandomGame() {
                print("Random game: (randomGame.name)")
            }
        }
    }
}
Listing 8-11

Adding operation to operate on videogames

This looks like it has a lot to unpack, but it’s very straightforward.

First, the class ViewController declaration is marked as @MainActor. In all the OS versions released with Xcode 13, we do not need to add the @MainActor attributes to classes, but I decided to add it so you can see a declaration can have multiple symbols that are run on different actors. Most of this class will run on the @MainActor, but the addRandomVideogames, removeRandomVideogame, and getRandomVideogame will all run on our new @MediaActor. Because they all have our own @MediaActor attribute, they can interact with the global videogames variable we declared earlier. And all of them will interact with it in a thread-safe manner because all of them are isolated to @MediaActor. It’s a beautiful system where we can mark distributed portions of our code to run on the same thread or actor, and all the internal synchronization of it will prevent us from introducing data races.

In viewDidLoad , we will run all videogame operations at once. We will add a videogame, we will remove a videogame, and then we will fetch one to print its info. It wouldn’t make sense to mark viewDidLoad as @MediaActor, and even if we tried to, we would be stopped because this method is known to modify your UI in most implementations. viewDidLoad must run on the main actor.

Because viewDidLoad runs on @MainActor and all our videogame operations run on @MediaActor, if we want to access one global actor from the other global actor, we need to do so asynchronously. For this reason, we create an asynchronous context with Task {}, and we need to either await on each videogame operation or call it concurrently with async let if it makes sense to do so. Fetching a videogame is also an asynchronous operation, so we can use await on the right side of the if let to call it.

Now, if we want to have another function that operates on these videogames, showAvailableVideogames, it can be anywhere. Suppose we have an imaginary Functions.swift file, completely independent of the ViewController and the actor declaration, we can just add the new function there, add the @MediaActor attribute, and anything running on the @MediaActor can interact with it synchronously and everything outside of it asynchronously. Listing 8-12 shows this new method.
@MediaActor
func showAvailableGames() {
    for game in videogames {
        print("(game.name)")
    }
}
Listing 8-12

The showAvailableGames method is in its own file, but it’s still isolated to the @MediaActor

Summary

Global Actors allow us to mark declarations spread across different files and other logical or physical locations, such as different frameworks, and have them run in the same thread. We say these declarations are isolated to the actor they are running on. The global actor will synchronize all its accesses, and for the developer, it is as easy as using the global actors as attributes to ensure they run on the same actor and avoid data races.

Apple provides the @MainActor global actor in all its platforms that have a UI. This includes iOS, macOS, watchOS, and tvOS. Every UI element, whether it comes from UIKit, AppKit, or SwiftUI, runs isolated to the main actor, and if we want to update or refresh our UI, we need to go the @MainActor before running these updates. This will ensure all accesses to the main thread are synchronized by the main actor, it will refresh our UI , and we will not see warnings or errors related to drawing our UI outside of the main thread.

We can create our own global actors for our own functionality. For example, if we had a framework that interacted with a database, we could add our own global attribute to all its classes to ensure all reads and writes happen in the same thread to avoid data races.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.111.118