项目场景:
项目基于C++实现,用到了状态机在不同状态之间切换来执行不同的流程。
问题描述
状态机在特定情况下无法再继续执行状态切换。
项目代码还是比较复杂的,所以这里只是摘取了关键代码段(当然看这个代码段可能很容易看出问题所在)。下面test里面的helloWorldFunc模拟实际的状态切换调用,从输出结果上"hello world"没有输出,所以并没有进入这个状态。
/******************************************************************************
Welcome to GDB Online.
GDB online is an online compiler and debugger tool for C, C++, Python, Java, PHP, Ruby, Perl,
C#, VB, Swift, Pascal, Fortran, Haskell, Objective-C, Assembly, HTML, CSS, JS, SQLite, Prolog.
Code, Compile, Run and Debug online from anywhere in world.
*******************************************************************************/
#include <iostream>
#include <memory>
#include <functional>
using namespace std;
struct transition {
using ptr = std::unique_ptr<transition>;
using executor = std::function<void()>;
transition(const executor& executor) : execute(executor) {
}
static void run(const executor& executor) {
transition::run(std::make_unique<transition>(executor));
}
static void run (transition::ptr transition) {
thread_local transition::ptr current;
if (current) {
// if in a transition already queue for execution after current is completed
current->next = std::move(transition);
return;
}
for (current = std::move(transition); current; current = std::move(current->next)) {
current->execute();
}
}
executor execute;
ptr next;
};
int main() {
auto helloWorldFunc = []() {
cout << "hello world" << endl;
};
auto exceptionFunc = []() {
throw std::exception();
};
try {
transition::run(exceptionFunc);
} catch (...) {
cout << "exception thrown" << endl;
}
try {
transition::run(helloWorldFunc);
} catch (...) {
cout << "exception thrown" << endl;
}
return 0;
}
原因分析:
注意看上面代码中用到了thread_local变量current来保留当前执行的状态,一般情况下状态执行完的话 current会释放,这样当切换下一个状态的时候可以正常执行。但是如果前一个状态transition::run(exceptionFunc)在执行current->execute()的时候如果抛出了异常,那么current无法被置为空,这样的话当执行transition::run(helloWorldFunc)的时候,这个状态会被添加到前一个状态的next里面,并且一直没有机会被执行,所以状态机再也无法切换状态了。
解决方案:
解决方案的话在执行current->execute()的时候加上异常捕获,当捕获到异常的时候将current置成空。
try {
for (current = std::move(transition); current; current = std::move(current->next)) {
current->execute();
}
} catch (...) {
current.reset();
}
总结:
- 在实际的项目中看这种问题还是有难度的,实际的项目中执行
transition::run的地方并没有加异常捕获,异常捕获的代码是加在一个thread pool里面的执行体里的,然后状态切换task被enqueue到这个thread pool来执行,幸好在捕获异常的地方输出了一行日志,不然我估计看不出来root cause。所以我们代码中如果加了异常捕获的话一定要加日志输出error信息方便看问题。 - 类似状态机这种可能会执行不同函数的场景,要多想想被执行的函数会不会throw exception,如果throw了exception要考虑好异常处理。