行为树BehaviorTree学习记录4_异步动作

(为免误导,特免责声明如下:本文所有内容,只是基于个人当前理解和实际做法,后面亦会有更正和修订,但任何版本都不免有个人能力有限、理解有误或者工作环境不同的状况,故文中内容仅供参考。任何人都可以借鉴或者直接使用代码片段,但对任何直接引用或者借鉴产生的技术问题等后果,作者不承担任何责任。)

异步动作

异步动作用在:需要比较长的时间来完成的行为,如果完成条件没有满足,会返回运行中。
- 异步动作节点不能阻塞tick太多时间,运行流程应该尽快返回。
- 如果halt()被调用,应尽快推出。

并发Concurrency 与并行Parallelism

并发是当两个或更多任务能在重叠的时间段内启动、运行和结束,并不意味着他们在同一时刻运行。
并行是不同的进程在同一时间都在运行,比如多核处理器。
BT.CPP executes all the nodes concurrently. In other words:
BT.CPP 同步执行所有节点,换句话说:

  • 树执行引擎是单线程的。
  • 所有的tick()方法是顺序执行的。
  • 如果那个tick()方法被阻塞了,则整个执行流程都阻塞在这里

我们通过“并发”和异步执行来实现反应性行为(reactive behaviors)。
或者说,如果一个行为需要执行很长时间,则应该立即返回running状态。

这就告诉树执行器这个行为已经开始了,但需要跟多时间来返回成功或者失败,我们需要再次tick这个节点来参看状态是否已经改变(轮询)。
一个异步节点可能委托这个长时间的工作到了另外一个进程(通过进程间通讯)或在其他线程。

异步和同步

总的来说,异步节点就是那种:

  • 被tick的时候,可能会返回running,而不是成功或者失败
  • 当触发halt()方法的时候,可以尽可能快的停止。

通常,开发者必须实现halt()方法。
当树执行一个异步行为节点,并返回运行中的时候,这个状态会向后回传,即整个树都会被认为处在运行状态。
下例中,“ActionE” 是个异步节点,并且正在运行中,当一个节点是运行中,通常其父节点也返回运行中。
在这里插入图片描述

下面看一个简单的SleepNode,StatefulActionNode是一个很好的异步行为节点的模版,可以继承它来生成自己的异步行为节点:

// Example of Asynchronous node that uses StatefulActionNode as base class
class SleepNode : public BT::StatefulActionNode
{
  public:
    SleepNode(const std::string& name, const BT::NodeConfig& config)
      : BT::StatefulActionNode(name, config)
    {}

    static BT::PortsList providedPorts()
    {
      // amount of milliseconds that we want to sleep
      return{ BT::InputPort<int>("msec") };
    }

    NodeStatus onStart() override
    {
      int msec = 0;
      getInput("msec", msec);

      if( msec <= 0 ) {
        // No need to go into the RUNNING state
        return NodeStatus::SUCCESS;
      }
      else {
        // once the deadline is reached, we will return SUCCESS.
        deadline_ = system_clock::now() + milliseconds(msec);
        return NodeStatus::RUNNING;
      }
    }

    /// method invoked by an action in the RUNNING state.
    NodeStatus onRunning() override
    {
      if ( system_clock::now() >= deadline_ ) {
        return NodeStatus::SUCCESS;
      }
      else {
        return NodeStatus::RUNNING;
      }
    }

    void onHalted() override
    {
      // nothing to do here...
      std::cout << "SleepNode interrupted" << std::endl;
    }

  private:
    system_clock::time_point deadline_;
};

上例中:

  • 当SleepNode第一次被tick的时候,onStart()方法会被调用,可能会立刻返回成功(如果sleep time是0),或者返回running(sleep time大于0)
  • 我们需要在循环里持续的tick这个树,这讲触发onRunning()这个方法,这个可能会再次返回running,或者到最后返回成功。(也可能失败,当然睡觉不会)
  • 其他节点可能触发以halt()信号,这时onHalted()方法会被调用。

避免阻塞树的执行

下面是一个SleepNode的错误实现方式。

// This is the synchronous version of the Node. Probably not what we want.
class BadSleepNode : public BT::ActionNodeBase
{
  public:
    BadSleepNode(const std::string& name, const BT::NodeConfig& config)
      : BT::ActionNodeBase(name, config)
    {}

    static BT::PortsList providedPorts()
    {
      return{ BT::InputPort<int>("msec") };
    }

    NodeStatus tick() override
    {  
      int msec = 0;
      getInput("msec", msec);
      // This blocking function will FREEZE the entire tree :(
      std::this_thread::sleep_for( milliseconds(msec) );
      return NodeStatus::SUCCESS;
     }

    void halt() override
    {
      // No one can invoke this method because I froze the tree.
      // Even if this method COULD be executed, there is no way I can
      // interrupt std::this_thread::sleep_for()
    }
};

多线程的问题,本库的以前版本(1.x),生成新线程看起来像是一个解决异步行为的好方法。
但却有以下问题:

  • 用线程安全的方式读写黑板会很困难(后面有更多说明)
  • You probably don’t need to.(不理解
  • 人们认为这会神奇地使Action“异步”,但忘记了在调用halt()方法时,他们仍然有责任“以某种方式”快速停止该线程。
    因为这些原因,并不鼓励使用BT::ThreadedAction作为基类,看下面的例子:
// This will spawn its own thread. But it still has problems when halted
class BadSleepNode : public BT::ThreadedAction
{
  public:
    BadSleepNode(const std::string& name, const BT::NodeConfig& config)
      : BT::ActionNodeBase(name, config)
    {}

    static BT::PortsList providedPorts()
    {
      return{ BT::InputPort<int>("msec") };
    }

    NodeStatus tick() override
    {  
      // This code runs in its own thread, therefore the Tree is still running.
      // This seems good but the thread still can't be aborted
      int msec = 0;
      getInput("msec", msec);
      std::this_thread::sleep_for( std::chrono::milliseconds(msec) );
      return NodeStatus::SUCCESS;
    }
    // The halt() method can not kill the spawned thread :(
};

halt()不能杀掉tick里面产生的线程。
正确的方式应该是:

// I will create my own thread here, for no good reason
class ThreadedSleepNode : public BT::ThreadedAction
{
  public:
    ThreadedSleepNode(const std::string& name, const BT::NodeConfig& config)
      : BT::ActionNodeBase(name, config)
    {}

    static BT::PortsList providedPorts()
    {
      return{ BT::InputPort<int>("msec") };
    }

    NodeStatus tick() override
    {  
      // This code runs in its own thread, therefore the Tree is still running.
      int msec = 0;
      getInput("msec", msec);

      using namespace std::chrono;
      const auto deadline = system_clock::now() + milliseconds(msec);

      // Periodically check isHaltRequested() 
      // and sleep for a small amount of time only (1 millisecond)
      while( !isHaltRequested() && system_clock::now() < deadline )
      {
        std::this_thread::sleep_for( std::chrono::milliseconds(1) );
      }
      return NodeStatus::SUCCESS;
    }

    // The halt() method will set isHaltRequested() to true 
    // and stop the while loop in the spawned thread.
};

可以看到,比第一次使用BT::StatefulActionNode的实现更加复杂,
这种模式有时候还是能用,但记得引入多线程会使得事情变复杂,所以尽量避免这么做。

Advanced example: client / server communication
高级示例,客户/服务器 通讯
经常BT.cpp会被用于在不同的进程里执行真实的任务,ROS里一个典型(并且推荐)的方式是使用ActionLib。
ActionLib提供一些我们所需用来正确实现异步行为的API:

  1. 一个非阻塞的函数启动该行为
  2. 有一个监控该行为当前执行状态的方法
  3. 一个获得执行结果或者错误信息的方法
  4. 有能力抢占或者终止正在执行中的行为。
    以上操作都是非阻塞的,所以不需要产生自己的新线程。

更为普遍的是,我们认为开发者有自己的进程间通信,采用一个客户/服务器的关系来实现BT执行器与真正的服务提供者之间的架构。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值