← Back to Blog
EN中文

机器人系统中的 Actor 模型与调度封装

在复杂的机器人控制系统中,传感器数据采集、路径规划、运动控制等任务往往需要并行处理。传统的基于共享内存和锁的并发模型在系统规模扩大时容易引入死锁和竞争条件,且难以调试。Actor 模型通过消息传递实现组件间的解耦,成为了一种优秀的替代方案。

本文将探讨如何在机器人系统中封装一个轻量级的 Actor 系统,并使用 Go 语言演示其核心实现。

为什么选择 Actor 模型?

机器人系统本质上是事件驱动的:激光雷达扫描到障碍物、里程计更新位置、上层指令下发目标点。这些事件需要在不同的模块间高效流转。

Actor 模型的核心思想是:

  1. 一切皆 Actor:每个 Actor 是一个独立的计算单元,拥有私有状态。
  2. 消息传递:Actor 之间通过发送消息进行通信,不共享内存。
  3. 并发执行:Actor 并发处理接收到的消息。

在 C++ 等底层语言中,我们通常需要手动管理线程池和消息队列。而在 Go 语言中,Goroutine 和 Channel 天生就是 Actor 模型的基石。

系统设计:静态捷径与生命周期

在实际工程中(参考某大型搜索公司的机器人库设计),为了方便业务代码调用,往往会封装一个全局或单例的 ActorSystem,并提供静态方法作为“捷径”(Static Shortcuts)。

这种设计的权衡在于:

  • 优点:业务代码无需传递复杂的上下文对象,随处可调用 Send 发送消息。
  • 缺点:引入了全局状态,对测试和模块化有一定挑战,但在嵌入式或单一应用的机器人进程中,这种简便性往往是值得的。

净室实现(Go)

我们将构建一个简化的 Actor 系统,包含以下要素:

  • PID (Process ID):唯一标识一个 Actor。
  • Actor 接口:定义接收消息的行为。
  • System:管理 Actor 的注册、销毁和消息分发。

1. 定义基础结构

package actor

import (
    "fmt"
    "sync"
)

// PID 唯一标识
type PID string

// Message 通用消息接口
type Message interface{}

// Actor 接收并处理消息的接口
type Actor interface {
    Receive(ctx *Context)
}

// Context 包含消息和上下文信息
type Context struct {
    Message Message
    Sender  PID
    Self    PID
}

// System 管理器
type System struct {
    actors map[PID]chan Message
    mu     sync.RWMutex
    wg     sync.WaitGroup
}

func NewSystem() *System {
    return &System{
        actors: make(map[PID]chan Message),
    }
}

2. Actor 的注册与启动

我们使用 Goroutine 来模拟 Actor 的运行循环。每个 Actor 拥有一个独立的 Channel 作为邮箱。

func (s *System) Spawn(name PID, actor Actor) error {
    s.mu.Lock()
    defer s.mu.Unlock()

    if _, exists := s.actors[name]; exists {
        return fmt.Errorf("actor %s already exists", name)
    }

    mailbox := make(chan Message, 100) // 带缓冲的邮箱
    s.actors[name] = mailbox
    s.wg.Add(1)

    go func() {
        defer s.wg.Done()
        for msg := range mailbox {
            actor.Receive(&Context{
                Message: msg,
                Self:    name,
            })
        }
    }()
    return nil
}

3. 消息分发(模拟 Static Shortcut)

在 C++ 实现中,TActorSystem::Send 常被封装为静态函数。在 Go 中,我们可以通过通过 System 实例方法实现类似效果。关键在于发送是非阻塞的(除非邮箱已满)。

func (s *System) Send(target PID, msg Message) {
    s.mu.RLock()
    ch, ok := s.actors[target]
    s.mu.RUnlock()

    if ok {
        select {
        case ch <- msg:
            // 发送成功
        default:
            fmt.Printf("Warning: Mailbox for %s is full, dropping message\n", target)
        }
    } else {
        fmt.Printf("Warning: Actor %s not found\n", target)
    }
}

工业场景应用

在机器人导航栈中,我们可以定义一个 Planner Actor 和一个 MotorController Actor。

type MoveCommand struct {
    X, Y float64
}

type MotorActor struct{}

func (m *MotorActor) Receive(ctx *Context) {
    switch msg := ctx.Message.(type) {
    case MoveCommand:
        fmt.Printf("[Motor] Moving to (%f, %f)\n", msg.X, msg.Y)
    }
}

func main() {
    sys := NewSystem()
    sys.Spawn("motor_1", &MotorActor{})

    // 模拟传感器数据触发路径规划,直接发送指令
    sys.Send("motor_1", MoveCommand{X: 10.5, Y: 20.0})
    
    // 实际系统中需要优雅关闭机制
}

这种模式有效地将“决策”(发送消息)与“执行”(处理消息)解耦。HTSwap 机制在 C++ 中用于无锁交换消息队列,而在 Go 中,Channel 底层已经为我们处理了同步问题,使得代码更加简洁。

通过封装调度逻辑,机器人各模块开发者只需关注 Receive 中的业务逻辑,而无需关心线程安全和锁竞争,这正是架构设计的价值所在。