📘

ループ内でなるべくActorを切り替えないようにする

2022/08/28に公開

はじめに

ループ内でActorを切り替えるときは、まとめてループを一つのActorで実行させたほうがいい。なぜならループ内でスレッドを切り替えることになり、その都度コンテキストスイッチが起こり、CPU利用率が上がってしまう。

内容

WWDC21: Swift concurrency: Behind the scenesで紹介されていた。

https://developer.apple.com/videos/play/wwdc2021/10254/

コードを見るとだいたいこんな感じで2つのアクターでスレッドが切り替わっている

  • updateArticlesメソッドはidsの個数分ループ
    • database.loadArticleメソッドがdatabaseアクターで動作
    • updateUIメソッド実行はメインスレッドに切り替わる
      • updateArticlesメソッドがMainActor指定されているため

しかもidsの個数制限がない。つまりこれが2億回くらい回ることもある。

これを改善するためにループで処理せずに配列をそのまままとめて処理するようにする。

これで2つのアクターは切り替えが最小限になる。

他にもWWDC21の動画で説明されていたことは

  • アクターはスレッドプールのスレッドを利用する
    • スレッドを生成しすぎというわけではない
      • 具体的には
        • idsが100あっても100個スレッドを使うわけじゃない

その他参考

What is actor hopping and how can it cause problems? hackingwithswift

https://www.hackingwithswift.com/quick-start/concurrency/what-is-actor-hopping-and-how-can-it-cause-problems

hackingwithswiftの記事。上記で説明したupdateUI的なことをやるとき、@Publishedなデータを都度appendしている例を示してる。そうなるとループごとにSwiftUI.View画面をリフレッシュするようなこともある。とすると単純にコンテキストスイッチの頻繁な切り替えよりもViewを更新してしまうことでそれによるリソースの使いすぎにもつながるのかもしれない。

@MainActor
class DataModel: ObservableObject {
    @Published var users = [User]()
    var database = Database()

    // Load all our users, updating the UI as each one
    // is successfully fetched
    func loadUsers() async {
        for i in 1...100 {
            let user = await database.loadUser(id: i)
            users.append(user)
        }
    }
}

感想

ベストを尽くすなら改善すりゃいいとは思う。けれどやるかどうかはもループ数によるとは思う。つまり例えば固定値で5回しか処理が繰り返さないと制限がコードで表現されているなら、計算量はたかが知れてるなーとは思う。その上で改善するかどうかは計測してみて判断する。

Discussion