(为免误导,特免责声明如下:本文所有内容,只是基于个人当前理解和实际做法,后面亦会有更正和修订,但任何版本都不免有个人能力有限、理解有误或者工作环境不同的状况,故文中内容仅供参考。任何人都可以借鉴或者直接使用代码片段,但对任何直接引用或者借鉴产生的技术问题等后果,作者不承担任何责任。)
异步动作
异步动作用在:需要比较长的时间来完成的行为,如果完成条件没有满足,会返回运行中。
- 异步动作节点不能阻塞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:
- 一个非阻塞的函数启动该行为
- 有一个监控该行为当前执行状态的方法
- 一个获得执行结果或者错误信息的方法
- 有能力抢占或者终止正在执行中的行为。
以上操作都是非阻塞的,所以不需要产生自己的新线程。
更为普遍的是,我们认为开发者有自己的进程间通信,采用一个客户/服务器的关系来实现BT执行器与真正的服务提供者之间的架构。