What is actor hopping and how can it cause problems? 什么是 actor 跳转,它如何导致问题?

What is actor hopping and how can it cause problems? 什么是 actor 跳转,它如何导致问题?

温馨提示:本文最后更新于2025-02-10 09:24:48,某些文章具有时效性,若有错误或已失效,请在下方留言

When a thread pauses work on one actor to start work on another actor instead, we call it actor hopping, and it will happen any time one actor calls another.
当线程暂停对一个 actor 的工作以开始对另一个 actor 的工作时,我们称之为 actor 跳跃,每当一个 actor 调用另一个 actor 时,它就会发生。

Behind the scenes, Swift manages a group of threads called the cooperative thread pool, creating as many threads as there are CPU cores so that we can’t be hit by thread explosion. Actors guarantee that they can be running only one method at a time, but they don’t care which thread they are running on – they will automatically move between threads as needed in order to balance system resources.
在幕后,Swift 管理着一组称为协作线程池的线程,创建的线程数与 CPU 内核数一样多,这样我们就不会受到线程爆炸的打击。Actor 保证它们一次只能运行一个方法,但它们并不关心它们在哪个线程上运行 – 它们将根据需要自动在线程之间移动,以平衡系统资源。

Actor hopping with the cooperative pool is fast – it will happen automatically, and we don’t need to worry about it. However, the main thread is not part of the cooperative thread pool, which means actor code being run from the main actor will require a context switch, which will incur a performance penalty if done too frequently.
Actor 跳槽与合作池的速度很快 – 它会自动发生,我们不需要担心。但是,主线程不是协作线程池的一部分,这意味着从主参与者运行的执行组件代码将需要上下文切换,如果切换过于频繁,将导致性能下降。

You can see the problem caused by frequent actor hopping in this toy example code:
您可以在以下玩具示例代码中看到频繁的 actor 跳跃导致的问题:

actor NumberGenerator {
    var lastNumber = 1

    func getNext() -> Int {
        defer { lastNumber += 1 }
        return lastNumber
    }

    @MainActor func run() async {
        for _ in 1...100 {
            let nextNumber = await getNext()
            print("Loading \(nextNumber)")
        }
    }
}

let generator = NumberGenerator()
await generator.run()
下载图标
what-is-actor-hopping-and-how-can-it-cause-problems-1.zip
zip文件
50.5K

In that code, the run() method must take place on the main actor because it has the @MainActor attribute attached to it, however the getNext() method will run somewhere on the cooperative pool, meaning that Swift will need to perform frequent context switching from to and from the main actor inside the loop.
在该代码中,run() 方法必须在主参与者上进行,因为它附加了 @MainActor 属性,但是 getNext() 方法将在合作池的某个位置运行,这意味着 Swift 将需要在循环中执行频繁的上下文切换。

In practice, your code is more likely to look like this:
在实践中,您的代码更有可能如下所示:

// An example piece of data we can show in our UI
struct User: Identifiable {
    let id: Int
}

// An actor that handles serial access to a database
actor Database {
    func loadUser(id: Int) -> User {
        // complex work to load a user from the database
        // happens here; we'll just send back an example
        User(id: id)
    }
}

// An object that handles updating our UI
@Observable @MainActor
class DataModel {
    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)
        }
    }
}

// A SwiftUI view showing all the users in our data model
struct ContentView: View {
    @State private var model = DataModel()

    var body: some View {
        List(model.users) { user in
            Text("User \(user.id)")
        }
        .task {
            await model.loadUsers()
        }
    }
}
下载图标
what-is-actor-hopping-and-how-can-it-cause-problems-2.zip
zip文件
26.0K

When that runs, the loadUsers() method will run on the main actor, because the whole DataModel class must run there – it has been annotated with @MainActor to avoid publishing changes from a background thread.
当它运行时,loadUsers() 方法将在主角色上运行,因为整个 DataModel 类必须在该处运行——它已经用 @MainActor 进行了注释,以避免从后台线程发布更改。

However, the database’s loadUser() method will run somewhere on the cooperative pool: it might run on thread 3 the first time it’s called, thread 5 the second time, thread 8 the third time, and so on; Swift will take care of that for us.
但是,数据库的 loadUser() 方法将在合作池中的某个位置运行:它可能第一次调用时在线程 3 上运行,第二次在线程 5 上运行,第三次在线程 8 上运行,依此类推;Swift 会为我们解决这个问题。

This means when our code runs it will repeatedly hop to and from the main actor, meaning there’s a significant performance cost introduced by all the context switching.
这意味着当我们的代码运行时,它将反复跳转到主 actor 或从 main actor 跳转,这意味着所有上下文切换都会带来显著的性能成本。

The solution here is to avoid all the switches by running operations in batches – hop to the cooperative thread pool once to perform all the actor work required to load many users, then process those batches on the main actor. The batch size could potentially load all users at once depending on your need, but even batch sizes of two would halve the context switches compared to individual fetches.
这里的解决方案是通过批量运行作来避免所有切换 – 跳转到协作线程池一次以执行加载许多用户所需的所有 actor 工作,然后在 main actor 上处理这些批处理。根据您的需要,批量大小可能会一次加载所有用户,但与单个获取相比,即使批量大小为 2 也会使上下文切换次数减半。

For example, we could rewrite our previous example like this:
例如,我们可以像这样重写前面的示例:

struct User: Identifiable {
    let id: Int
}

actor Database {
    func loadUsers(ids: [Int]) -> [User] {
        // complex work to load users from the database
        // happens here; we'll just send back examples
        ids.map(User.init)
    }
}

@Observable @MainActor
class DataModel {
    var users = [User]()
    var database = Database()

    func loadUsers() async {
        let ids = Array(1...100)

        // Load all users in one hop
        let newUsers = await database.loadUsers(ids: ids)

        // Now back on the main actor, update the UI
        users.append(contentsOf: newUsers)
    }
}

struct ContentView: View {
    @State private var model = DataModel()

    var body: some View {
        List(model.users) { user in
            Text("User \(user.id)")
        }
        .task {
            await model.loadUsers()
        }
    }
}
下载图标
what-is-actor-hopping-and-how-can-it-cause-problems-3.zip
zip文件
26.0K

Notice how the SwiftUI view is identical – we’re just rearranging our internal data access to be more efficient.
请注意 SwiftUI 视图是如何相同的 – 我们只是在重新安排内部数据访问以提高效率。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享