ROS2多线程Python版本

本文详细记录了在ROS2环境下使用Gazebo模拟AGV时遇到的多线程问题。作者最初尝试用while循环来控制AGV旋转,导致回调函数被阻塞。通过将移动函数与odom_callback分开并尝试使用不同的Executor和CallbackGroup,最终通过多线程方式解决了问题。具体解决方案涉及到SingleThreadedExecutor和MultiThreadedExecutor的使用,以及ReentrantCallbackGroup和MutuallyExclusiveCallbackGroup的区别和应用。

接着上篇文章

ROS2使用Gazebo控制AGV

,我记录一下ROS2多线程Python版的问题。

提出这个话题的原因是上文中,当需要小车旋转90°时。我最开始的想法是用while循环来实现:

while abs(self.first_yaw-self.yaw)>0.05:
    cmd_vel = Twist()
    cmd_vel.linear.x = 0.0
    cmd_vel.angular.z = angle
    self.cmd_vel_pub.publish(cmd_vel)   

而不是用if :

if abs(self.first_yaw-self.yaw)>0.05:
cmd_vel = Twist()
cmd_vel.linear.x = 0.0
cmd_vel.angular.z = angle
self.cmd_vel_pub.publish(cmd_vel)

显然,使用while会该回调函数 odom_callback 堵塞,导致无法正常获得 self.yaw ,于是小车一直打转 。 如上文,使用if 可以正常运行。

知晓while 出问题的原因,我尝试将整个移动函数与odom_callback分开,另写一个回调函数执行移动函数, 然而这是没用的, while依旧会堵塞主线程

最终通过多线程的方式解决问题 :

#! /usr/bin/env python3
import rclpy
import math
import numpy as np
from rclpy.node import Node
from geometry_msgs.msg import Twist
from nav_msgs.msg import Odometry
import time
from rclpy.callback_groups import ReentrantCallbackGroup, MutuallyExclusiveCallbackGroup
from rclpy.executors import MultiThreadedExecutor, SingleThreadedExecutor

def euler_from_quaternion(quaternion):
    """
    Converts quaternion (w in last place) to euler roll, pitch, yaw
    quaternion = [x, y, z, w]
    """
    x = quaternion.x
    y = quaternion.y
    z = quaternion.z
    w = quaternion.w

    sinr_cosp = 2 * (w * x + y * z)
    cosr_cosp = 1 - 2 * (x * x + y * y)
    roll = np.arctan2(sinr_cosp, cosr_cosp)

    sinp = 2 * (w * y - z * x)
    pitch = np.arcsin(sinp)

    siny_cosp = 2 * (w * z + x * y)
    cosy_cosp = 1 - 2 * (y * y + z * z)
    yaw = np.arctan2(siny_cosp, cosy_cosp)

    return roll, pitch, yaw

class CarController(Node):
    def __init__(self):
        super().__init__('car_controller')

        # 目标位置
        self.target_x = 1.0  # 目标 x 坐标
        self.target_y = 1.0  # 目标 y 坐标
        self.current_x = 0.0  # 当前 x 坐标
        self.current_y = 0.0  # 当前 y 坐标
        self.roll=0.0
        self.pitch=0.0
        self.yaw=0.0
        self.group1 = MutuallyExclusiveCallbackGroup()
        self.group2 = MutuallyExclusiveCallbackGroup()

        # 订阅当前位置
        self.odom_sub = self.create_subscription(
            Odometry,
            '/odom',
            self.odom_callback,
            10, callback_group=self.group1
        )
        self.odom_sub_1 = self.create_subscription(
            Odometry,
            '/odom',
            self.odom_callback_1,
            10, callback_group=self.group2
        )
        # 发布控制指令
        self.cmd_vel_pub = self.create_publisher(Twist, '/cmd_vel', 10)

        # 标记当前步骤
        self.current_step = 1

        # 角速度
        self.angular_velocity = math.pi / 2  # 90 度/秒
        self.timer=None
        self.first_yaw =None
        self.timer_count=0

        



    def odom_callback(self, odom_msg):
        self.current_x = odom_msg.pose.pose.position.x
        self.current_y = odom_msg.pose.pose.position.y
        orientation=odom_msg.pose.pose.orientation
        self.roll, self.pitch, self.yaw = euler_from_quaternion(orientation)
        self.get_logger().info('Current Position: x={:.2f}, y={:.2f}, roll={:.2f}, pitch={:.2f}, yaw={:.2f}'.format(self.current_x, self.current_y, self.roll, self.pitch, self.yaw))

        

    def odom_callback_1(self,odom_msg):

        if self.current_step == 1:  # 走完 x 轴
            if abs(self.current_x - self.target_x) < 0.05:
                self.get_logger().info('X direction target position reached!')
                self.current_step = 2  # 进入第二步:旋转
                self.stop_car()
                return

            
            cmd_vel = Twist()
            if self.current_x < self.target_x:
                cmd_vel.linear.x = 0.5  # 正向线速度,可根据需要进行调整
            else:
                cmd_vel.linear.x = -0.5  # 反向线速度,可根据需要进行调整

            self.cmd_vel_pub.publish(cmd_vel)

        elif self.current_step == 2 and abs(self.current_y - self.target_y) > 0.05:  # 根据 y 轴坐标差选择旋转方向
            dy = self.target_y - self.current_y
            if dy > 0:
                angle = math.pi / 2  # 向左旋转 90 度
            else :
                angle = -math.pi / 2  # 向右旋转 90 度
            
            if not self.first_yaw:
                self.first_yaw=self.yaw+angle

            while abs(self.first_yaw-self.yaw)>0.05:
                cmd_vel = Twist()
                cmd_vel.linear.x = 0.0
                cmd_vel.angular.z = angle
                self.cmd_vel_pub.publish(cmd_vel)   


            self.stop_car()
            self.current_step = 3      

        elif self.current_step == 3:  # 走完 y 轴
            if abs(self.current_y - self.target_y) < 0.05:
                self.get_logger().info('Target position reached!')
                self.stop_car()
                return

            cmd_vel = Twist()
            cmd_vel.linear.x = 0.5  # 前进线速度,可根据需要进行调整
            self.cmd_vel_pub.publish(cmd_vel)


    def stop_car(self):
        cmd_vel = Twist()
        self.cmd_vel_pub.publish(cmd_vel)


def main(args=None):
    rclpy.init(args=args)
    car_controller = CarController()
    # executor = MultiThreadedExecutor(num_threads=2)
    executor = SingleThreadedExecutor()
    # Add the node to the executor
    executor.add_node(car_controller)
    try:
        executor.spin()
    finally:
        # Shutdown the executor
        executor.shutdown()
        car_controller.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

记录一下多线程吧,主要是回调函数的多线程,ROS2中的多线程主要由Executors和Callback Groups组成

1.Executors

ROS2中的执行器分为两类SingleThreadedExecutor 和 MultiThreadedExecutor

SingleThreadedExecutor:同一时刻只能执行一个线程的任务,其他任务需要排队,这个是ROS2默认的执行器,其实当你使用rclpy.spin()时,使用的就是默认的Executor(你可以开两个线程同时执行rclpy.spin()试试 )

这也是导致为什么while 执行时,其他回调任务也阻塞了。

MultiThreadedExecutor: 允许同时执行多个线程的任务 。当然,直接开启多个线程执行回调函数是没有用的,为什么,就要引出Callback Groups了

2.Callback Groups

ROS2中的执行器分为两类ReentrantCallbackGroup 和 MutuallyExclusiveCallbackGroup。

两者的区别就是ReentrantCallbackGroup允许,同一时刻,一个Group中多个线程同时执行 ?

而MutuallyExclusiveCallbackGroup,同一时刻,一个Group中只能有一个线程执行。而 ROS2中,默认的就是这个回调组,而且不在订阅时声明callback_group=self.group,那么所有

的订阅都在同一个回调组中进行 。因此想要真正使用多线程,需要:

    self.group1 = MutuallyExclusiveCallbackGroup()
    self.group2 = MutuallyExclusiveCallbackGroup()
    self.odom_sub = self.create_subscription(
        Odometry, 'odom', self.odom_callback, 10,  callback_group=self.group1)
    
    executor = MultiThreadedExecutor(num_threads=3)

或者将group声明为ReentrantCallbackGroup,那么只用这样一个组就够了。

想要尝试不同Executor 和 Callback Group的效果,可以执行下面的代码:

自己给参数运行 ,类似: ros2 run xxx_pkg xxx -service_wait_time 5.0 -timer_period 1.0 -callback_group_type reentrant -threads 2

from pickle import TRUE
from std_srvs.srv import SetBool
import rclpy
from rclpy.node import Node
import time

from rclpy.callback_groups import ReentrantCallbackGroup, MutuallyExclusiveCallbackGroup
from rclpy.executors import MultiThreadedExecutor, SingleThreadedExecutor
from rclpy.qos import ReliabilityPolicy, QoSProfile

import argparse


class DummyServer(Node):

    def __init__(self, args, callback_group_type="reentrant"):

        self.timer_flag = True

        super().__init__('service_start_turn')

        # More info here: https://docs.python.org/3/library/argparse.html
        parser = argparse.ArgumentParser(
            description='Dummy Server to Learn about Callback Groups and Threads')

        # Add an argument for setting the service waiting time
        parser.add_argument('-service_wait_time',
                            type=float,
                            help='Time the service will be waiting',
                            required=True)

        # Add an argument for setting por of the timer callback
        parser.add_argument('-timer_period',
                            type=float,
                            nargs=1,
                            metavar='TIMEOUT',
                            default=1.0,
                            help="Time period of the Callback for the timer")

        # Add an argument for setting the callback group type
        parser.add_argument('-callback_group_type',
                            type=str,
                            default="reentrant",
                            help="Type of Callback Groups REENTRANT of EXCLUSIVE")

        # Add an argument for setting the number of threads
        parser.add_argument('-threads',
                            type=int,
                            default=1,
                            help="Number of threads to use in the executor")

        self.args = parser.parse_args(args[1:])

        parser.print_help()

        print("## DEFAULT Node Callback Group=" +
            str(self.default_callback_group))

        self.get_logger().warning("Setting "+self.args.callback_group_type+" Groups")
        if self.args.callback_group_type == "reentrant":
            # If you set the group reentrant, any Callback inside will be executed in parallel
            # If there are enough threads
            self.group1 = ReentrantCallbackGroup()
            self.get_logger().warning("ReentrantCallbackGroup Set")
            # Both the service and the timer use the same callback group
            self.srv = self.create_service(
                SetBool, '/dummy_server_srv', self.SetBool_callback, callback_group=self.group1)
            self.timer = self.create_timer(
                self.args.timer_period[0], self.timer_callback, callback_group=self.group1)

        elif self.args.callback_group_type == "exclusive":
            self.group1 = MutuallyExclusiveCallbackGroup()
            self.group2 = MutuallyExclusiveCallbackGroup()
            self.get_logger().warning("MutuallyExclusiveCallbackGroup Set")
            # Set one group for the service and another one for the timer
            self.srv = self.create_service(
                SetBool, '/dummy_server_srv', self.SetBool_callback, callback_group=self.group1)
            self.timer = self.create_timer(
                self.args.timer_period[0], self.timer_callback, callback_group=self.group2)

        else:
            # You do not set groups. Therefore, they will get the default group for the Node
            self.get_logger().error("NO GROUPS SET Set")
            self.srv = self.create_service(
                SetBool, '/dummy_server_srv', self.SetBool_callback)
            self.timer = self.create_timer(
                self.args.timer_period[0], self.timer_callback)

    def get_threads(self):
        return self.args.threads

    def SetBool_callback(self, request, response):
        self.get_logger().warning("Processing Server Message...")
        self.wait_for_sec(self.args.service_wait_time)
        self.get_logger().warning("Processing Server Message...DONE")
        response.message = 'TURNING'
        # return the response parameters
        return response

    def wait_for_sec(self, wait_sec, delta=1.0):
        i = 0
        while i < wait_sec:
            self.get_logger().info("..."+str(i)+"[WAITING...]")
            time.sleep(delta)
            i += delta

    def timer_callback(self):
        self.print_dummy_msgs()

    def print_dummy_msgs(self):
        if self.timer_flag:
            self.get_logger().info("TICK")
            self.timer_flag = False
        else:
            self.get_logger().info("TACK")
            self.timer_flag = True


def main(args=None):

    rclpy.init(args=args)
    print("args==="+str(args))
    args_without_ros = rclpy.utilities.remove_ros_args(args)
    print("clean ROS args==="+str(args_without_ros))
    start_stop_service_node = DummyServer(args_without_ros)

    num_threads = start_stop_service_node.get_threads()
    start_stop_service_node.get_logger().info(
        'DummyServer Started with threads='+str(num_threads))

    executor = MultiThreadedExecutor(num_threads=num_threads)
    executor.add_node(start_stop_service_node)

    try:
        executor.spin()
    finally:
        executor.shutdown()
        start_stop_service_node.destroy_node()

    # shutdown the ROS communication
    rclpy.shutdown()


if __name__ == '__main__':
    main()
### ROS2多线程的使用方法 在 ROS2 的架构下,为了提高系统的并发性能和响应速度,引入了更灵活的多线程机制。不同于 ROS1,在 C++ 和 Python 下的不同行为模式,ROS2 提供了一致性的多线程支持[^3]。 #### 创建多线程节点 要创建一个多线程ROS2 节点,可以通过设置 `rclcpp::NodeOptions` 对象来指定执行器(Executor)。默认情况下,ROS2 使用单线程执行器;然而,当涉及到多个回调函数时,推荐使用多线程执行器以提升效率: ```cpp #include "rclcpp/rclcpp.hpp" #include "std_msgs/msg/string.hpp" int main(int argc, char * argv[]) { rclcpp::init(argc, argv); auto options = rclcpp::NodeOptions(); // 设置为多线程执行器 rclcpp::executors::MultiThreadedExecutor exec; std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("multi_thread_node", options); // 添加更多逻辑... } ``` #### 配置执行器类型 除了上述提到的方法外,还可以直接配置执行器类型的参数来进行初始化工作。这允许开发者更加精细地控制哪些部分应该采用何种形式的工作流策略: ```cpp // 初始化多线程执行器实例 auto executor = rclcpp::executors::MultiThreadedExecutor(); // 将节点加入到执行器中 executor.add_node(node); while (true){ try { // 执行所有已注册的任务直到遇到错误为止 executor.spin_some(); } catch (...) { /* 错误处理 */ } } ``` #### 线程安全注意事项 尽管 ROS2 支持多线程环境下的开发,但在编写代码过程中仍需注意数据竞争条件等问题。确保共享资源访问的安全性至关重要,比如利用互斥锁或其他同步原语保护临界区内的操作。 #### 实际案例分析 考虑到实际应用场景的需求,例如传感器读取、消息发布与订阅等功能模块之间的协作关系,合理规划各组件间的交互方式能够有效减少延迟并增强稳定性。对于复杂度较高的项目来说,适当增加线程数量有助于分担计算负担,从而获得更好的实时表现效果[^1]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mubibaiwhale

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值