WWDC Quick Look 💓 By SwiftGGTeam
Meet distributed actors in Swift

Meet distributed actors in Swift

观看原视频

Highlight

Swift 5.7 的 distributed actor 把 actor 模型从单进程扩展到多进程场景,通过编译器检查的序列化要求和位置透明性,让远程调用和本地调用写法完全一致。

核心内容

从本地 Actor 到分布式 Actor

你的井字棋游戏目前只有单机模式。人类玩家和 AI 对手都是本地 actor,通过异步消息通信。你想加入多人对战功能,让两个设备上的玩家互相连线。

以前的做法是手动处理 WebSocket 连接、消息序列化、状态同步。代码里充斥着 encode/decode、重连逻辑、心跳检测,游戏逻辑被网络代码淹没。

00:30)Distributed actor 的核心思路是:actor 已经通过消息传递来通信了,为什么不能把这些消息发送到另一台设备上?

Swift actor 的”并发之海”模型里,每个 actor 是一座孤岛,通过异步消息交换数据。Distributed actor 把这个模型扩展到”分布式之海”:每座孤岛可以位于不同的设备、服务器或进程中。

迁移第一步:把 Actor 变成 Distributed Actor

04:47)先看原来的本地 actor:

public actor BotPlayer: Identifiable {
    nonisolated public let id: ActorIdentity = .random

    var ai: RandomPlayerBotAI
    var gameState: GameState

    public init(team: CharacterTeam) {
        self.gameState = .init()
        self.ai = RandomPlayerBotAI(playerID: self.id, team: team)
    }

    public func makeMove() throws -> GameMove {
        return try ai.decideNextMove(given: &gameState)
    }

    public func opponentMoved(_ move: GameMove) async throws {
        try gameState.mark(move)
    }
}

06:11)改成 distributed actor 只需要几步:

import Distributed

public distributed actor BotPlayer: Identifiable {
    typealias ActorSystem = LocalTestingDistributedActorSystem

    var ai: RandomPlayerBotAI
    var gameState: GameState

    public init(team: CharacterTeam, actorSystem: ActorSystem) {
        self.actorSystem = actorSystem
        self.gameState = .init()
        self.ai = RandomPlayerBotAI(playerID: self.id, team: team)
    }

    public distributed func makeMove() throws -> GameMove {
        return try ai.decideNextMove(given: &gameState)
    }

    public distributed func opponentMoved(_ move: GameMove) async throws {
        try gameState.mark(move)
    }
}

关键点:

  • distributed actor 替代 actor,自动符合 DistributedActor 协议
  • ActorSystem 类型别名声明这个 actor 使用的分布式系统
  • actorSystem 是编译器合成的属性,必须在初始化器中赋值
  • id 属性由 actor system 自动分配,不能手动声明
  • distributed func 标记可以远程调用的方法

位置透明性

03:41)Distributed actor 最重要的特性是位置透明性(location transparency)。无论 actor 在本地还是远程,调用代码完全一样:

// 本地 actor
let localBot = BotPlayer(team: .fish, actorSystem: system)
let move = try await localBot.makeMove()

// 远程 actor(通过 resolve 获取引用)
let remoteBot = try BotPlayer.resolve(id: opponentID, using: webSocketSystem)
let move = try await remoteBot.makeMove()  // 语法完全相同!

关键点:

  • resolve(id:using:) 获取远程 actor 的引用,不是创建实例
  • 返回的引用和本地实例使用完全相同的 API
  • 网络、序列化、传输细节全部由 actor system 处理

服务器端实现

13:35)把 BotPlayer 移到服务器上运行,客户端通过 WebSocket 连接:

import Distributed
import TicTacFishShared

@main struct Boot {
    static func main() {
        let system = try! SampleWebSocketActorSystem(
            mode: .serverOnly(host: "localhost", port: 8888)
        )

        system.registerOnDemandResolveHandler { id in
            if system.isBotID(id) {
                return system.makeActorWithID(id) {
                    OnlineBotPlayer(team: .rodents, actorSystem: system)
                }
            }
            return nil
        }

        try await server.terminated
    }
}

关键点:

  • registerOnDemandResolveHandler 实现按需创建 actor
  • 客户端发来的消息指向的 ID 可能还没有对应实例
  • 服务器根据 ID 特征判断是否需要创建新的 BotPlayer

本地网络对战:Receptionist 模式

16:32)除了客户端/服务器模式,distributed actor 也支持点对点(peer-to-peer)通信。在本地 Wi-Fi 环境下,两个设备直接发现对方并建立游戏。

发现远程 actor 使用 receptionist 模式:

let opponentTeam = model.team == .fish ? CharacterTeam.rodents : CharacterTeam.fish

let listing = await localNetworkSystem.receptionist.listing(
    of: OpponentPlayer.self,
    tag: opponentTeam.tag
)

for try await opponent in listing where opponent.id != self.player.id {
    model.foundOpponent(opponent, myself: self.player, informOpponent: true)
    return
}

关键点:

  • receptionist.listing(of:tag:) 返回一个 AsyncSequence
  • 新设备加入网络时,listing 会自动产生新值
  • for try await 遍历发现的对手

20:23)人类玩家作为 distributed actor 时,需要处理远程调用本地 UI 的情况:

public distributed actor LocalNetworkPlayer: GamePlayer {
    public typealias ActorSystem = SampleLocalNetworkActorSystem

    let team: CharacterTeam
    let model: GameViewModel

    public distributed func makeMove() async -> GameMove {
        let field = await model.humanSelectedField()
        movesMade += 1
        let move = GameMove(
            playerID: self.id,
            position: field,
            team: team,
            teamCharacterID: movesMade % 2
        )
        return move
    }
}

关键点:

  • makeMove() 被远程调用时,需要等待人类玩家在本地 UI 上操作
  • await model.humanSelectedField() 挂起远程调用,直到用户点击
  • 返回的 GameMove 通过 actor system 自动序列化传回远程调用方

详细内容

Distributed Actor 的编译器检查

Distributed actor 比普通 actor 多了三层编译器检查:

// 1. 只有 distributed 方法可以远程调用
public distributed func makeMove() throws -> GameMove  // OK
public func helper() { }  // 不能在远程引用上调用

// 2. 参数和返回值必须满足序列化要求
distributed func sendMessage(_ text: String)  // OK,String 是 Codable
distributed func sendImage(_ image: UIImage)  // 编译错误!UIImage 不是 Codable

// 3. Actor system 类型必须匹配
let bot: BotPlayer  // BotPlayer 的 ActorSystem 是 LocalTestingDistributedActorSystem
let remoteBot = try BotPlayer.resolve(id: id, using: otherSystem)  // 类型不匹配会报错

关键点:

  • distributed 关键字显式标记远程 API 边界
  • 默认序列化要求是 Codable,actor system 可以自定义
  • 编译器在编译期就保证了远程调用的类型安全

Actor System 的职责

Actor system 是 distributed actor 的核心抽象,负责:

  1. 分配 ID:每个 actor 创建时分配全局唯一标识符
  2. 序列化/反序列化:把方法调用编码为网络消息
  3. 路由:根据 ID 找到目标 actor 的位置
  4. 传输:通过网络发送消息
// Swift 5.7 内置的本地测试系统
typealias ActorSystem = LocalTestingDistributedActorSystem

// 自定义 WebSocket 系统
typealias ActorSystem = SampleWebSocketActorSystem

// 开源的集群系统(SwiftNIO 实现)
// https://github.com/apple/swift-distributed-actors/

序列化要求

10:21)Distributed actor 的所有 distributed func 的参数和返回值类型必须满足 actor system 的序列化要求。默认是 Codable

struct GameMove: Codable {
    let playerID: ActorIdentity
    let position: Int
    let team: CharacterTeam
    let teamCharacterID: Int
}

如果 actor system 使用其他序列化方案(如 Protocol Buffers),可以声明不同的要求:

distributed actor BotPlayer {
    typealias ActorSystem = ProtobufActorSystem
    // 此时参数/返回值需要符合 ProtobufSerializable
}

核心启发

1. 用 distributed actor 替代手动网络层

如果你正在用 WebSocket、URLSession 或第三方库实现设备间通信,考虑用 distributed actor 重构。游戏状态同步、协作编辑、实时聊天等场景特别适合。从 LocalTestingDistributedActorSystem 开始,本地测试通过后再接入真实网络。

入口:import Distributeddistributed actor

2. 构建本地多人游戏体验

利用 Network 框架 + distributed actor 实现零服务器成本的本地多人游戏。Receptionist 模式自动发现附近设备,不需要用户手动输入 IP 或扫码。

入口:receptionist.listing(of:tag:)SampleLocalNetworkActorSystem

3. 用 actor system 抽象不同的部署模式

同一套游戏逻辑可以用不同的 actor system 部署:本地测试用 LocalTestingDistributedActorSystem,局域网对战用自定义 Network 框架系统,在线对战用 WebSocket 系统,大规模集群用 Swift Distributed Actors 库。业务代码完全不用改。

入口:typealias ActorSystem = ...resolve(id:using:)

4. 服务器端 Swift 的新选择

Distributed actor 为服务器端 Swift 提供了类型安全的 RPC 抽象。相比 Vapor 的路由处理器,distributed actor 让客户端和服务器共享接口定义,编译器保证两端类型一致。

入口:SampleWebSocketActorSystem(mode: .serverOnly)registerOnDemandResolveHandler

关联 Session

评论

GitHub Issues · utterances