Thread-Per-Message Pattern

什么是Thread-Per-Message Pattern?

设想一个场景,妻子在忙着淘宝,对丈夫说,“亲爱的,能不能帮我倒一下垃圾。”,然后老公去倒垃圾,老婆继续逛淘宝,这就是Thread-Per-Message Pattern,拜托别人,“这件事就交给你了”以后再回来做自己的事。

对每个命令或者请求,分配一个线程,由这个线程执行工作。


现在有这样的需求,启动三个线程,分别输出10个A,20个B,30个C,允许字母交叉,首先看代码:

首先写一个Helper类,负责对字母进行输出:

package threadPerMessage;
public class Helper {
    public void handle(int count, char c) {
        System.out.println("        handle(" + count + ", " + c + ") BEGIN");
        for (int i = 0; i < count; i++) {
            slowly();
            System.out.print(c);
        }
        System.out.println("");
        System.out.println("        handle(" + count + ", " + c + ") END");
    }
    private void slowly() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
    }
}

然后写一个Host类,作为Helper的宿主类,对外提供helper的方法:

package threadPerMessage;

public class Host {
    private final Helper helper = new Helper();
    public void request(final int count, final char c) {
        System.out.println("    request(" + count + ", " + c + ") BEGIN");
        new Thread() {
            public void run() {
                helper.handle(count, c);
            }
        }.start();
        System.out.println("    request(" + count + ", " + c + ") END");
    }
}
最后写一个测试类Test:

package threadPerMessage;

public class Test {
    public static void main(String[] args) {
        System.out.println("main BEGIN");
        Host host = new Host();
        host.request(10, 'A');
        host.request(20, 'B');
        host.request(30, 'C');
        System.out.println("main END");
    }
}
回过头来看代码,在Host类中我们发现了一个匿名内部类,建立了Thread子类的实例,并且启动线程:

new Thread(){
    @Override
    public void run(){
        helper.handle(count,c);
    }
}.start();
实际上是把类声明,建立实例,启动线程写在了一起。
匿名内部类:将类的声明和建立实例的操作写在一起,执行方法的时候才建立类文件。匿名类和一般类相同,都会在编译时产生出类文件。当我们在匿名内部类中用到方法的参数或者局部变量时,必须将变量声明成final,如果不是final,会发生编译错误。


Thread-Per-Message Pattern的所有参与者

1、Client(委托人)参与者

Client参与者对Host参与者发出请求,Client不知道Host如何实现这个请求,但知道他会去做。

2、Host参与者

当Host收到Client发出的请求的时候,会建立新的线程启动它,这个新的线程,会使用Helper参与者,处理这个请求。

3、Helper(帮助者)参与者

Helper参与者会对Host参与者提供处理请求的功能,Host参与者所建立的线程,会使用Helper参与者。


什么时候使用这个模式?

1、提升响应度,降低延迟时间

使用这个模式,Host对Client的响应度会提高,延迟时间会下降,尤其当处理函数很花时间或者需要等待I/O的时候,效果很明显。

2、适合在操作顺序无所谓时使用

这个模式中,handle方法执行顺序不一定时request方法调用顺序。

3、不需要返回值的时候

这个模式中,request方法不会等到handle方法执行结束,也就是说request方法拿不到handle方法的返回值。

4、应用在服务器的制作

这个模式可以在客户端送达请求,主线程接收,其他线程处理该请求的情况下使用。

5、调用方法+启动线程→传送消息

通常调用出普通方法的时候,执行完方法里的操作,控制权才会回来,然而在这个模式里request时期待才做开始的触发器,但是不会等待到执行结束(传送异步消息)。


进阶说明:进程与线程

进程与线程之间的关系,会因为平台的差异,有极大的不同,但是我们一般可以说一个进程可以建立多个线程。

线程的内存时共享的。

线程和进程最大的差异在于内存是否能共享,通常进程的内存空间是各自独立的,进程不能擅自读取、改写其他进程的内存空间。因为进程内存空间的独立性,进程无须担心被其他进程破坏。

线程则是共享内存的,所以我们进程在一条线程的内存上写入数据,而其他线程来读取。这里说的共享相同的内存,在Java中就体现为共享相同的实例。

因为线程间的内存是共享的,因此线程之间的沟通可以很自然、简单地做到。然而一位内同一个实例可以有多个线程同时访问,所以需要正确地进行互斥共享。

线程间的context-switch较容易

进程和线程的另一个差异,在于contex-switch的繁重程度。

要切换进程的时候,进程需要将当前自己的状态(context信息)存储下来,并将下一个要开始执行的进程以前所保留的context数据读回来,这个信息切换操作(context switch)所需要花费一些时间。

切换执行线程的时候,线程也需要进行context-switch操作,然而,线程所管理的context信息比进程要少,一般而言线程的context-swicth要比进程的context-switch快得多。所以需要紧密进行多项相关工作得时候,线程通常比进程来得实用。

PS、Java的内存模型中,将内存分为主存储器和工作内存两种,能够让线程共享的,只有主存储器部分。

import datetime import pathlib import threading import time import hydra import numpy as np import torch import zmq # ZeroMQ用于进程间通信 from typing import Optional import dill # 用于Python对象序列化 from omegaconf import OmegaConf # 配置管理库 # 导入自定义模块 from agent.schema import RobotState, RobotObsShape # 机器人状态和观察数据结构,消息定义发送 from agent.utils import ( dict_apply, # 递归应用函数到字典中的张量 interpolate_image_batch, # 批量调整图像尺寸并进行归一化 ) from controller.policy.dexgraspvla_controller import DexGraspVLAController # 抓取控制策略 import logging import cv2 import os # 获取当前脚本所在目录 current_dir = os.path.dirname(os.path.abspath(__file__)) # 构建相对路径 pretrain_path = os.path.join(current_dir, "..", "..", "..", "dinov2", "checkpoints","dinov2_vitb14_pretrain.pth") # 初始化日志系统 def log_init(): """配置并初始化日志记录器""" logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) # 设置日志级别 # 定义日志格式 format = ( "[%(asctime)s %(levelname)s %(filename)s %(funcName)s:%(lineno)d] %(message)s" ) handler = logging.StreamHandler() # 控制台日志处理器 handler.setLevel(logging.DEBUG) formatter = logging.Formatter(format, datefmt="%Y-%m-%d %H:%M:%S") handler.setFormatter(formatter) logger.addHandler(handler) logger.info("Logger inited") return logger logger = log_init() # 全局日志记录器 class Robot: """机器人控制主类,负责状态管理、观察获取和动作执行""" def __init__(self, config): """ 根据配置初始化机器人系统 参数: config: 包含以下关键字段的配置对象 - controller_checkpoint_path: 控制器模型路径 - device: 计算设备 (cpu/cuda) - port: ZeroMQ通信端口 - executions_per_action_chunk: 每个动作块执行次数 """ # 加载预训练控制器模型 checkpoint_path = config.controller_checkpoint_path payload = torch.load(checkpoint_path, pickle_module=dill) # 使用dill加载模型 #使用 `torch.load` 加载模型检查点,但指定了 `pickle_module=dill`。这通常是因为模型保存时使用了 `dill` 库而不是标准的 `pickle` 库。`dill` 可以序列化更广泛的对象,包括一些 `pickle` 无法处理的函数、lambda 表达式等。 # 更新模型配置中的本地权重路径 # payload["cfg"]["policy"]["obs_encoder"]["model_config"]["head"][ # "local_weights_path" # ] = "/home/fishros/.cache/torch/hub/checkpoints/dinov2_vitb14_pretrain.pth" payload["cfg"]["policy"]["obs_encoder"]["model_config"]["head"][ "local_weights_path" ] = pretrain_path cfg = payload["cfg"] cls = hydra.utils.get_class(cfg._target_) # 动态获取类 workspace = cls(cfg) # 创建工作空间实例 workspace.load_payload(payload, exclude_keys=None, include_keys=None) # 加载模型权重 # 初始化控制器 self.controller: DexGraspVLAController self.controller = workspace.model # 配置参数 self.device = config.device self.executions_per_action_chunk = config.executions_per_action_chunk # 设置模型为评估模式并转移到指定设备 self.controller.eval() print(f'!! torch.cuda.is_available()={torch.cuda.is_available()}') self.controller.to(self.device if torch.cuda.is_available() else "cpu") # 传感器数据初始化 self.head_image = None # 头部摄像头图像 self.wrist_image = None # 腕部摄像头图像 self.proprioception = None # 本体感知数据(关节角度等) # ZeroMQ通信设置 self.context = zmq.Context() self.socket = self.context.socket(zmq.REP) # 应答模式socket self.port = config.port self.socket.bind(f"tcp://*:{self.port}") # 绑定到指定端口 # 状态管理 self.state = RobotState.IDLE # 初始状态为空闲 # 重置动作和监听线程 self.resetting_action = None self.listening_thread = threading.Thread(target=self.listening_mannual) self.listening_thread.start() # 启动用户输入监听线程 def _parse_obs(self, message: bytes) -> Optional[dict]: """解析从socket接收的二进制观察数据""" # 验证消息长度 if len(message) != RobotObsShape.CHUNK_SIZE: logger.error( f"Invalid message size, required {RobotObsShape.CHUNK_SIZE} bytes" ) return None # 解析头部摄像头图像数据 (uint8数组) head_image = np.frombuffer( message.buffer[: RobotObsShape.HEAD_IMAGE_SIZE], dtype=np.uint8, ).reshape(RobotObsShape.HEAD_IMAGE_SHAPE) # 解析腕部摄像头图像数据 wrist_image = np.frombuffer( message.buffer[RobotObsShape.HEAD_IMAGE_SIZE : RobotObsShape.HEAD_IMAGE_SIZE+ RobotObsShape.WRIST_IMAGE_SIZE], dtype=np.uint8, ).reshape(RobotObsShape.WRIST_IMAGE_SHAPE) # 解析本体感知数据 (float32数组) proprioception = np.frombuffer( message.buffer[-RobotObsShape.STATE_SIZE :], dtype=np.float32, ).reshape(RobotObsShape.STATE_SHAPE) logger.info("Received head_image, wrist_image, and joint_angle") return { "head_image": head_image, "wrist_image": wrist_image, "proprioception": proprioception, } def listening_mannual(self) -> None: """监听用户输入线程函数,用于手动控制状态""" logger.info("Robot is listening...") while True: user_input = input("Press <Enter> or <q> to quit: ") if user_input == "q": self.state = RobotState.FINISHED # 退出程序 elif user_input == "i": self.state = RobotState.INITALIZING # 初始化状态 elif user_input == "r": self.state = RobotState.RESETTING # 重置状态 elif user_input == "f": self.state = RobotState.FINISHED # 结束状态 else: logger.info("Invalid input. Please press <Enter> or <q>.") def _initialize(self) -> None: """初始化机器人到准备抓取位置""" assert self.state == RobotState.INITALIZING logger.info("Initializing robot...") # 实际实现中这里会包含机械臂的初始化移动 self.state = RobotState.ACTING # 进入执行状态 logger.info("Robot initialized") def _reset_socket(self) -> None: """重置ZeroMQ socket连接""" logger.info("Resetting socket...") self.socket.close() self.context.term() # 重新创建socket self.context = zmq.Context() self.socket = self.context.socket(zmq.REP) self.socket.bind(f"tcp://*:{self.port}") logger.info("Socket reset") def _reset(self) -> None: """任务完成后重置机器人到初始位置""" assert self.state == RobotState.RESETTING logger.info("Resetting robot...") # 实际实现中这里会包含机械臂的复位移动 self.state = RobotState.ACTING logger.info("Robot reset") def _get_obs(self) -> Optional[dict]: """获取并预处理观察数据""" logger.info("Waiting for obs...") message = self.socket.recv(copy=False) # 接收观察数据 obs = self._parse_obs(message) if obs is None: self._reset_socket() # 解析失败时重置socket return None # 更新传感器数据 self.head_image = obs["head_image"] self.wrist_image = obs["wrist_image"] self.proprioception = obs["proprioception"] #self.head_image = cv2.imread("/home/fishros/hdx/tool/dataset_ori/imgs/0_130.jpg") #self.wrist_image = cv2.imread("/home/fishros/hdx/tool/dataset_ori/imgs/0_130.jpg") #self.proprioception = np.array([ 244.02, 39.33, 17.21, 291.47, 119.56, 75.05, 0.8], dtype=np.float32) #self.proprioception = np.array([ 188.07692307692307,47.12087912087912,-3.1868131868131866,311.56043956043953,156.26373626373626,64.46153846153847,1], dtype=np.float32) # 图像预处理 (插值和维度转换) rgb_head = interpolate_image_batch(self.head_image[None, ...]).unsqueeze(0) rgb_wrist = interpolate_image_batch(self.wrist_image[None, ...]).unsqueeze(0) logger.info("Robot state updated") return { "rgb": rgb_head, # (1,1,3,H,W) "right_cam_img": rgb_wrist, # (1,1,3,H,W) "right_state": torch.from_numpy(self.proprioception) .unsqueeze(0) .unsqueeze(0), # (1,1,6) } def act(self, obs: dict) -> bool: """使用控制器模型预测并发送动作""" # 将观察数据转移到模型设备 obs = dict_apply(obs, lambda x: x.to(self.controller.device)) # 模型推理 (无梯度计算) with torch.no_grad(): actions = self.controller.predict_action(obs_dict=obs) # (B,64,action_dim) # 处理动作数据 n_latency_steps = 3 # 延迟补偿步数 actions = ( actions.detach() .cpu() .numpy()[ 0, n_latency_steps : self.executions_per_action_chunk + n_latency_steps ] # (executions_per_action_chunk, action_dim) ) # 通过socket发送动作 logger.info(f"Sent action {actions}") self.socket.send(actions.tobytes()) return True def step(self) -> bool: """单步执行:获取观察->执行动作""" logger.info("Waiting for obs...") obs = self._get_obs() if obs is None: logger.error("Broken obs") return False logger.info("Robot state updated, acting...") if not self.act(obs): logger.error("Failed to send action") return False logger.info("Action sent, waiting for next obs...") return True def run(self) -> None: """机器人主控制循环""" logger.info("Robot loop starting...") assert self.state == RobotState.IDLE self.state = RobotState.INITALIZING # 状态机主循环 while True: logger.info(f"run loop with robot state: {self.state}") if self.state == RobotState.INITALIZING: self._initialize() elif self.state == RobotState.RESETTING: self._reset() elif self.state == RobotState.ACTING: self.step() # 执行主要控制循环 elif self.state == RobotState.FINISHED: logger.info("Robot loop finished, waiting for next command") # 可在此处添加等待新指令的逻辑 else: logger.error("Robot loop in unknown state.") break # OmegaConf解析器注册 def now_resolver(pattern: str): """处理${now:}时间格式化的解析器""" return datetime.now().strftime(pattern) # 注册自定义解析器 OmegaConf.register_new_resolver("now", now_resolver, replace=True) OmegaConf.register_new_resolver("eval", eval, replace=True) @hydra.main(version_base=None,config_path="config", config_name=pathlib.Path(__file__).stem) def main(cfg): """程序入口点:初始化并运行机器人""" robot = Robot(cfg) robot.run() if __name__ == "__main__": main()这是发送actions的服务端,主要为8组关节角和夹爪状态,我这边接收端的消息解析有点问题, 输出的action都是none def parse_actions(self, action_bytes): """解析接收到的动作数据""" # 验证消息长度 if len(action_bytes) != RobotObsShape.ACTIONS_SHAPE: return None actions = np.frombuffer(action_bytes, dtype=np.float32) return actions.reshape(RobotObsShape.ACTIONS_SHAPE)
最新发布
07-19
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值