🦁

【翻訳】@MainActor and Global Actors in Swift

2023/07/16に公開

最近、actorについて、actorとは何か、actorの使い方についてお話ししました。
覚えていれば、actorはプロパティへのアクセスを制御するので、
メンバーが異なるプロセスから同時に書き込まれることはなく、データの破損を防ぐことができます。

メイン・スレッドがすべてだ。

アップル・プラットフォーム用のプログラミングをしばらくやっている人も、初めてやる人も、メイン・スレッドについて聞いたことがあるかもしれません。メインスレッドはUIコードの実行を担当します。アップル・プラットフォームでは、メイン・スレッド以外の場所でUIを更新することは許されていません。一般的に非同期なプロセスを実行している場合、実行中のどのスレッドでも値を返す可能性がありますが、その結果をメインスレッドに届ける必要が
あります。最新の同時実行システムが導入される前は、DispatchQueue.main.asyncを呼び出し、
完了ブロックを渡すだけでよかった。このブロックはmain上で実行されるので、そこからUIを更新しても
安全です。もちろん、これはメイン・スレッドですべてを行おうとするべきだという意味ではありません。
メイン・スレッドが本当に忙しければ、ユーザーにとって目に見えるパフォーマンスの問題になるし、
アプリが応答しなくなれば、システムは定義された時間後にそのアプリを終了させるからです。

新しい同時実行システムは、タスクを中断したり、他のタスクを再開したり
(異なるスレッドで再開することもある)、異なるスレッドを飛び回る可能性があるため、
メイン・スレッドを更新する別のメカニズムが必要です。
このメカニズムは存在し、@MainActorと呼ばれる "特別な "種類のactorです。

main actorの紹介

main actorは @MainActor と書き、メインスレッドを表します。
main actorはすべての同期をメインディスパッチキューで行います。
このアクタは "特別" なのは、Apple のフレームワークの至る所で見つけることができるからです。
SwiftUI、AppKit、UIKit、watchKit...メインスレッドで実行する必要がある場所の数は膨大で、
メインスレッドの同期を必要とするこれらのフレームワーク内の個々のUIクラスのことを考えてもいません。
すべてのビューやビューコントローラがメインスレッドで動作する必要があるため、
あらゆる場所から@MainActorにアクセスする必要性が高まります。

main actorを使用するには、@MainActor 属性を定義に追加する必要があります。
メソッドでもクラスでもかまいません。
関数に@MainActorを追加すると、その関数は常にメインスレッドで実行されます。

@MainActor func fetchGames() {

}

上記の例では、fetchGamesは常にmain actor上で実行されます。
こうすることで、将来のプログラマは、このコードがメインスレッドで実行されることになっていることを
常に知ることができ、推測を働かせて、より分かりやすいコードを書くことができます。

メインスレッドの外で@MainActorメソッドを呼び出す場合は、awaitする必要があります。

await fetchGames()

クラスのような大きな定義に @MainActor 属性を追加すると、
すべてのプロパティとメソッドが MainActor になります。
個々のメソッドは、nonisolated キーワードを採用することで、
main actorの一部にならないことを選択できます。

@MainActor
class GameLibraryViewController: UIViewController {
	//...
	nonisolated var fetchVideogameTypes() -> [VideogameType] { ... }
	//...
}

MainActorは本当に重要なコンセプトで、これを正しく使うことを学べば、
Appleが提供するどのUIフレームワークでも、
より簡単にモダンな同時実行システムを採用できるようになります。
幸運なことに、その使い方は簡単で、気にする必要のあるマジックや隠れた動作はありません。

Global Actors

先に、main actorは "特別な "actorであると述べました。
そして、それはある種そうなのですが、その種類はそれだけではありません。
MainActorはGlobal Actorと呼ばれるactorの一種であることが分かりました。

UIコンポーネントが文字通り、あちこちにあるのはご存知でしょう。異なるフレームワークがそれらを持ち、
ファイルや異なるインポートにまたがって見つかるかもしれません。
MainActor を UI と連動させるには、必要なときに誰もが使えるアクタを作成する方法が必要です。
Global Actorは、その名の通り、グローバルに宣言され、それを採用したいすべてのオブジェクトは、@MainActor class MyClassThatRunsOnMainActor のように属性として追加するだけです。

Xcode 13, Beta 3からは、独自の目的のために独自のGlobal Actorを定義することができます。

注:Xcode 13, beta 3のリリースノートは、
global actorの存在と使用について言及した最初のものです。
以前のリリースノートでは触れられていなかったし、
WWDC2021の同時実行に関するセッションでも触れられていませんでした。
Xcode 13の以前のベータ版でglobal actorを使用できたかどうかは知りませんが、
私はこのような小さな好奇心が好きなので、このことについて触れています。

Global Actorの作成

Global Actorの作成方法は以下の通りです:

@globalActor
struct MediaActor {
  actor ActorType { }

  static let shared: ActorType = ActorType()
}

ここで、MediaActorは私たち自身が付けた名前である。
そして、@MainActorのように、宣言の前にその名前を追加することで、
採用したい全ての型やメソッド、あるいはモジュールが採用できるようになります。

一度に複数の場所に書き込んだり読み込んだりできるグローバル配列があるとします。
そのグローバル変数は@MediaActorで帰属させることができ、
それに対するすべての操作は同じスレッド上で実行され、actorは必要に応じて状態を同期させる。

次の例では、グローバルな videogames 配列を作成し、それをさまざまな場所から更新します。

まず、GlobalState というファイルを作成して、
global actor、グローバル変数、ビデオゲーム構造体を宣言します。

// GlobalState.swift

@globalActor
struct MediaActor {
  actor ActorType { }

  static let shared: ActorType = ActorType()
}

struct Videogame {
    let id = UUID()
    let name: String
    let releaseYear: Int
    let developer: String
}

@MediaActor var videogames: [Videogame] = []

重要な注意:

このような方法でグローバル変数を使用することは容認できませんし、
抽象化するにはもっと良い方法があります。このプロジェクトは、global actorについて
学ぶためのものであり、それ以上でもそれ以下でもないことを忘れないでください。

次に、デフォルトですべてがmain actorで実行されるViewControllerを作成します。

// ViewController.swift

@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)")
            }
        }
    }
}

この例を選んだのは、ViewController自体は@MainActor上で実行され、
デフォルトではそのプロパティとメソッドもすべて実行されるからです。
しかし、グローバルなvideogames変数とやりとりするのであれば、
MediaActorでこれらのメソッドを実行する必要があります。
videogamesと同じアクタで実行する必要がある、ViewControllerの3つの操作(addRandomVideogames()、removeRandomvideogame、getRandomGame())は、
MediaActorとしてマークするだけで実行できます。

この@MediaActorのデータに@MainActorからアクセスする必要がある場合、
メソッドは暗黙的にasyncとしてマークされているので、これらのメソッドでawaitする必要があります。

ここまでで、@MainActorはいろいろな場所にあります。2つの異なるファイルからアクセスできるだけでなく、
異なる宣言からもアクセスできます。最後に、Functions.swiftというファイルを作成し、
そこに@MediaActor上で動作する関数を1つ置きます。

// Functions.swift

@MediaActor
func showAvailableGames() async {
    for game in videogames {
        print("\(game.name)")
    }
}

それで終わりです!
独自のglobal actorを実装するのがいかに簡単か、お分かりいただけるでしょう。

結論

MainActor はglobal actorです。すべてのUIコードはmain actor上で実行されます。
異なるスレッドで実行されるかもしれませんが、メインスレッドで実行する必要があるコードを実行する場合、
メソッドを @MainActor としてマークし、そのメソッドでデータを受け取ることができます。

global actorは、物理的に異なるファイルにある宣言や、
異なる宣言にまたがる宣言などをマークできるので便利です。
異なるファイルや型間でステートを同期する必要がある場合は、独自のglobal actorを作成できます。
global actorの宣言は簡単で、その上で実行することに関心のある宣言は、
@属性として採用するだけです。

以下に、独自のglobal actorを使用するサンプル・プロジェクトを示します。

【翻訳元の記事】

@MainActor and Global Actors in Swift
https://www.andyibanez.com/posts/mainactor-and-global-actors-in-swift/

Discussion