状态机咋无法切换状态了。。。

135 阅读2分钟

项目场景:

项目基于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();
        }

总结:

  1. 在实际的项目中看这种问题还是有难度的,实际的项目中执行transition::run的地方并没有加异常捕获,异常捕获的代码是加在一个thread pool里面的执行体里的,然后状态切换task被enqueue到这个thread pool来执行,幸好在捕获异常的地方输出了一行日志,不然我估计看不出来root cause。所以我们代码中如果加了异常捕获的话一定要加日志输出error信息方便看问题。
  2. 类似状态机这种可能会执行不同函数的场景,要多想想被执行的函数会不会throw exception,如果throw了exception要考虑好异常处理。