Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / Objective C

C++ State Machine with Threads

Rate me:
Please Sign up or sign in to vote.
4.74/5 (34 votes)
26 Jan 2019CPOL7 min read 58.2K   1.8K   65   11
A framework combining state machines and multicast asynchronous callbacks

Introduction

A software-based Finite State Machines (FSM) is an implementation method used to decompose a design into states and events. Simple embedded devices with no operating system employ single threading such that the state machines run on a single “thread”. More complex systems use multithreading to divvy up the processing.

Many FSM implementations exist including one I wrote about here on Code Project entitled “State Machine Design in C++”. The article covers how to create C++ state machines using the StateMachine base class. What is missing, however, is how to integrate multiple state machines into the context of a multithreaded environment.

Asynchronous Multicast Callbacks with Inter-Thread Messaging” is another article I wrote on Code Project. This design provides a simple, portable callback mechanism that handles the low-level details of asynchronously invoking a callback with event data on a client-specified thread of control.

This article combines the two previously described techniques, state machines and asynchronous callbacks, into a single project. In the previous articles, it may not be readily apparent using simple examples how multiple state machines coordinate activities and dispatch events to each other. The goal for the article is to provide a complete working project with threads, timers, events, and state machines all working together. To illustrate the concept, the example project implements a state-based self-test engine utilizing asynchronous communication between threads.

I won’t be re-explaining the StateMachine and AsyncCallback<> implementations as the prior articles do that already. The primary focus is on how to combine the state machine and asynchronous callbacks into a single framework.

Visual Studio 2008 and 2015 projects are included for easy experimentation. While the Windows operating system provides threads, locks, message queues, and timers, the code is partitioned for easy porting to other embedded or PC-based systems.

Self-Test Subsystem

Self-tests execute a series of tests on hardware and mechanical systems to ensure correct operation. In this example, there are four state machine classes implementing our self-test subsystem as shown in the inheritance diagram below:

Image 1

Figure 1: Self-Test Subsystem Inheritance Diagram

Asynchronous Callbacks

The AsyncCallback<> class is used throughout to provide asynchronous callbacks. The first place it's used is within the SelfTest class. Whenever a self-test completes, a SelfTest::CompletedCallback callback is invoked notifying registered clients. SelfTestEngine registers with both CentrifugeTest and PressureTest to get informed when the test is complete.

The second location is the user interface registers with SelfTestEngine::StatusCallback. This allows a client, running on another thread, to register and receive status callbacks during execution. AsyncCallback<> allows the client to specify the exact callback thread making it easy to avoid cross-threading errors.

The final location is within the Timer class which fires periodic callbacks on a registered callback function. A generic, low-speed timer capable of calling a function on the client-specified thread is quite useful for event driven state machines where you might want to poll for some condition to occur. In this case, the Timer class is used to inject poll events into the state machine instances.

SelfTestEngine

SelfTestEngine is thread-safe and the main point of contact for client’s utilizing the self-test subsystem. CentrifugeTest and PressureTest are members of SelfTestEngine. SelfTestEngine is responsible for sequencing the individual self-tests in the correct order as shown in the state diagram below:

Image 2

Figure 2: SelfTestEngine State Machine

The Start event initiates the self-test engine. SelfTestEngine::Start() is an asynchronous function relying upon StartCallback to invoke the private SelfTestEngine::StartPrivateCallback() event function. Since Start() is asynchronous, it is thread-safe to be called by any client running on any thread.

C++
void SelfTestEngine::Start()
{
    // Asynchronously call StartPrivateCallback
    StartCallback(NoData());
}

void SelfTestEngine::StartPrivateCallback()
{
    BEGIN_TRANSITION_MAP                                    // - Current State -
        TRANSITION_MAP_ENTRY (ST_START_CENTRIFUGE_TEST)     // ST_IDLE
        TRANSITION_MAP_ENTRY (CANNOT_HAPPEN)                // ST_COMPLETED
        TRANSITION_MAP_ENTRY (CANNOT_HAPPEN)                // ST_FAILED
        TRANSITION_MAP_ENTRY (EVENT_IGNORED)                // ST_START_CENTRIFUGE_TEST
        TRANSITION_MAP_ENTRY (EVENT_IGNORED)                // ST_START_PRESSURE_TEST
    END_TRANSITION_MAP(NULL)
}

When each self-test completes, the Complete event fires causing the next self-test to start. After all of the tests are done, the state machine transitions to Completed and back to Idle. If the Cancel event is generated at any time during execution, a transition to the Failed state occurs.

The SelfTest base class provides three states common to all SelfTest-derived state machines: Idle, Completed, and Failed. SelfTestEngine then adds two more states: StartCentrifugeTest and StartPressureTest.

SelfTestEngine has one public event function, Start(), that starts the self-tests. SelfTestEngine::StatusCallback is an asynchronous callback allowing clients to register for status updates during testing. A WorkerThread instance is also contained within the class. All self-test state machine execution occurs on this thread.

C++
class SelfTestEngine : public SelfTest
{
public:
    // Clients register for asynchronous self-test status callbacks
    static AsyncCallback<SelfTestStatus> StatusCallback;

    // Singleton instance of SelfTestEngine
    static SelfTestEngine& GetInstance();

    // Start the self-tests
    void Start();

    WorkerThread& GetThread() { return m_thread; }
    static void InvokeStatusCallback(std::string msg);

private:
    AsyncCallback<> StartCallback;
    void StartPrivateCallback();

    SelfTestEngine();
    void Complete();

    // Sub self-test state machines
    CentrifugeTest m_centrifugeTest;
    PressureTest m_pressureTest;

    // Worker thread used by all self-tests
    WorkerThread m_thread;

    // State enumeration order must match the order of state method entries
    // in the state map.
    enum States
    {
        ST_START_CENTRIFUGE_TEST = SelfTest::ST_MAX_STATES,
        ST_START_PRESSURE_TEST,
        ST_MAX_STATES
    };

    // Define the state machine state functions with event data type
    STATE_DECLARE(SelfTestEngine,     StartCentrifugeTest,      NoEventData)
    STATE_DECLARE(SelfTestEngine,     StartPressureTest,        NoEventData)

    // State map to define state object order. Each state map entry defines a
    // state object.
    BEGIN_STATE_MAP
        STATE_MAP_ENTRY(&Idle)
        STATE_MAP_ENTRY(&Completed)
        STATE_MAP_ENTRY(&Failed)
        STATE_MAP_ENTRY(&StartCentrifugeTest)
        STATE_MAP_ENTRY(&StartPressureTest)
    END_STATE_MAP

    // Declare state machine events that receive async callbacks
    CALLBACK_DECLARE_NO_DATA(SelfTestEngine,    StartPrivateCallback)
    CALLBACK_DECLARE_NO_DATA(SelfTestEngine,    Complete)
    CALLBACK_DECLARE_NO_DATA(SelfTest,          Cancel)
};

As mentioned previously, the SelfTestEngine registers for asynchronous callbacks from each sub self-tests (i.e., CentrifugeTest and PressureTest) as shown below. When a sub self-test state machine completes, the SelfTestEngine::Complete() function is called. When a sub self-test state machine fails, the SelfTestEngine::Cancel() function is called.

C++
SelfTestEngine::SelfTestEngine() :
    SelfTest(ST_MAX_STATES),
    m_thread("SelfTestEngine")
{
    StartCallback.Register(&SelfTestEngine::StartPrivateCallback, &m_thread, this);

    // Register for callbacks when sub self-test state machines complete or fail
    m_centrifugeTest.CompletedCallback.Register(&SelfTestEngine::Complete, &m_thread, this);
    m_centrifugeTest.FailedCallback.Register(&SelfTestEngine::Cancel, &m_thread, this);
    m_pressureTest.CompletedCallback.Register(&SelfTestEngine::Complete, &m_thread, this);
    m_pressureTest.FailedCallback.Register(&SelfTestEngine::Cancel, &m_thread, this);
}

The SelfTest base class generates the CompletedCallback and FailedCallback within the Completed and Failed states respectively as seen below:

C++
STATE_DEFINE(SelfTest, Completed, NoEventData)
{
    SelfTestEngine::InvokeStatusCallback("SelfTest::ST_Completed");

    // Generate asynchronous completed callback if client is registered
    if (CompletedCallback)
        CompletedCallback(NoData());

    InternalEvent(ST_IDLE);
}

STATE_DEFINE(SelfTest, Failed, NoEventData)
{
    SelfTestEngine::InvokeStatusCallback("SelfTest::ST_Failed");

    // Generate asynchronous failed callback if client registered
    if (FailedCallback)
        FailedCallback(NoData());

    InternalEvent(ST_IDLE);
}

One might ask why the state machines use asynchronous callbacks. If the state machines are on the same thread, why not use a normal, synchronous callback instead? The problem to prevent is a callback into a currently executing state machine, that is, the call stack wrapping back around into the same class instance. For example, the following call sequence should be prevented: SelfTestEngine calls CentrifugeTest calls back SelfTestEngine. An asynchronous callback allows the stack to unwind and prevents this unwanted behavior.

CentrifugeTest

The CentrifugeTest state machine diagram shown below implements the centrifuge self-test described in "State Machine Design in C++". The difference here is that the Timer class is used to provide Poll events via asynchronous callbacks.

Image 3

Figure 3: CentrifugeTest State Machine

Timer

The Timer class provides a common mechanism to receive function callbacks by registering with Expired. Start() starts the callbacks at a particular interval. Stop() stops the callbacks.

C++
class Timer
{
public:
    static const DWORD MS_PER_TICK;

    /// An expired callback client's register with to get callbacks
    AsyncCallback<> Expired;

    /// Starts a timer for callbacks on the specified timeout interval.
    /// @param[in]    timeout - the timeout in milliseconds.
    void Start(DWORD timeout);

    /// Stops a timer.
    void Stop();

    ...

All Timer instances are stored in a private static list. The WorkerThread::Process() loop periodically services all the timers within the list by calling Timer::ProcessTimers(). Client’s registered with Expired are invoked whenever the timer expires.

C++
case WM_USER_TIMER:
    Timer::ProcessTimers();
    break;

Poll Events

CentrifugeTest has a Timer instance and registers for callbacks. The callback function, a thread instance and a this pointer is provided to Register() facilitating the asynchronous callback mechanism.

C++
// Register for timer callbacks
m_pollTimer.Expired.Register(&CentrifugeTest::Poll, &SelfTestEngine::GetInstance().GetThread(), this);

When the timer is started using Start(), the Poll() event function is periodically called at the interval specified.

C++
void CentrifugeTest::Poll()
{
    BEGIN_TRANSITION_MAP                                    // - Current State -
        TRANSITION_MAP_ENTRY (EVENT_IGNORED)                // ST_IDLE
        TRANSITION_MAP_ENTRY (EVENT_IGNORED)                // ST_COMPLETED
        TRANSITION_MAP_ENTRY (EVENT_IGNORED)                // ST_FAILED
        TRANSITION_MAP_ENTRY (EVENT_IGNORED)                // ST_START_TEST
        TRANSITION_MAP_ENTRY (ST_WAIT_FOR_ACCELERATION)     // ST_ACCELERATION
        TRANSITION_MAP_ENTRY (ST_WAIT_FOR_ACCELERATION)     // ST_WAIT_FOR_ACCELERATION
        TRANSITION_MAP_ENTRY (ST_WAIT_FOR_DECELERATION)     // ST_DECELERATION
        TRANSITION_MAP_ENTRY (ST_WAIT_FOR_DECELERATION)     // ST_WAIT_FOR_DECELERATION
    END_TRANSITION_MAP(NULL)
}

STATE_DEFINE(CentrifugeTest, Acceleration, NoEventData)
{
    SelfTestEngine::InvokeStatusCallback("CentrifugeTest::ST_Acceleration");

    // Start polling while waiting for centrifuge to ramp up to speed
    m_pollTimer.Start(10);
}

Connecting Callbacks to Event Functions

The AsyncCallback<> mechanism is able to invoke a static member function or a free function. However, state machine events are implemented using instance member functions. The key to connecting the two is the CALLBACK_DECLARE and CALLBACK_DECLARE_NO_DATA macros.

CentrifugeTest.h has this line:

C++
CALLBACK_DECLARE_NO_DATA(CentrifugeTest, Poll)

The first macro argument is the class name. The second is the instance event function name. The macro is defined as:

C++
#define CALLBACK_DECLARE_NO_DATA(stateMachine, eventName) \
    private:\
    static void eventName(const NoData& data, void* userData) { \
        ASSERT_TRUE(userData != NULL); \
        stateMachine* stateMachine##Instance = static_cast<stateMachine*>(userData); \
        stateMachine##Instance->eventName(); } 

Expanding the CALLBACK_DECLARE_NO_DATA macro above yields:

C++
private:
    static void Poll(const NoData& data, void* userData) {
        ASSERT_TRUE(userData != NULL);
        CentrifugeTest* CentrifugeTestInstance = static_cast<CentrifugeTest*>(userData);
        CentrifugeTestInstance->Poll(); }

There is no magic here. A static member Poll() function is created that accepts the AsyncCallback<> callback. The void* userData comes from 3rd argument of the Register() function and, in this case, is an instance of CentrifugeTest. The void* is typed to CentrifugeTest* so that the instance member Poll() may be called.

In this case, the “NO_DATA” macro version is used since the Poll() event doesn’t accept an argument. To connect a callback to an event function with event data, the CALLBACK_DELCARE macro is used as shown below:

C++
CALLBACK_DECLARE(MyStateMachine, MyEventFunc, MyEventFuncData)

Of course, you could do all this without the multiline macro, but it cleans up monotonous code that would otherwise be propagated throughout the project.

User Interface

The project doesn’t have a user interface except the text console output. For this example, the “user interface” just outputs self-test status messages on the user interface thread via the SelfTestEngineStatusCallback() function:

C++
WorkerThread userInterfaceThread("UserInterface");

void SelfTestEngineStatusCallback(const SelfTestStatus& status, void* userData)
{
    // Output status message to the console "user interface"
    cout << status.message.c_str() << endl;
}

Before the self-test starts, the user interface registers with the SelfTestEngine::StatusCallback callback.

C++
SelfTestEngine::StatusCallback.Register(&SelfTestEngineStatusCallback, &userInterfaceThread);

The user interface thread here is just used to simulate callbacks to a GUI library normally running in a separate thread of control.

Run-Time

The program’s main() function is shown below. It creates the two threads, registers for callbacks from SelfTestEngine, then calls Start() to start the self-tests.

C++
int main(void)
{
    // Create the worker threads
    userInterfaceThread.CreateThread();
    SelfTestEngine::GetInstance().GetThread().CreateThread();

    // Register for self-test engine callbacks
    SelfTestEngine::StatusCallback.Register(&SelfTestEngineStatusCallback, &userInterfaceThread);
    SelfTestEngine::GetInstance().CompletedCallback.Register
                    (&SelfTestEngineCompleteCallback, &userInterfaceThread);

    // Start the worker threads
    ThreadWin::StartAllThreads();

    // Start self-test engine
    SelfTestEngine::GetInstance().Start();

    // Wait for self-test engine to complete
    while (!selfTestEngineCompleted)
        Sleep(10);

    // Exit the worker threads
    userInterfaceThread.ExitThread();
    SelfTestEngine::GetInstance().GetThread().ExitThread();

    return 0;
}

SelfTestEngine generates asynchronous callbacks on the UserInteface thread. The SelfTestEngineStatusCallback() callback outputs the message to the console.

C++
void SelfTestEngineStatusCallback(const SelfTestStatus& status, void* userData)
{
    // Output status message to the console "user interface"
    cout << status.message.c_str() << endl;
}

The SelfTestEngineCompleteCallback() callback sets a flag to let the main() loop exit.

C++
void SelfTestEngineCompleteCallback(const NoData& data, void* userData)
{
    selfTestEngineCompleted = TRUE;
}

Running the project outputs the following console messages:

Image 4

Figure 4: Console Output

References

Conclusion

The StateMachine and AsycCallback<> implementations can be used separately. Each is useful unto itself. However, combining the two offers a novel framework for multithreaded state-driven application development. The article has shown how to coordinate the behavior of state machines when multiple threads are used, which may not be entirely obvious when looking at simplistic, single threaded examples.

I’ve successfully used ideas similar to this on many different PC and embedded projects. The code is portable to any platform with a small amount of effort. I particularly like the idea of asynchronous callbacks because it effectively hides inter-thread communication and the organization of the state machines makes creating and maintaining self-tests easy.

History

  • 18th November, 2016
    • Initial release
  • 19th November, 2016
    • Minor article corrections
  • 25th November, 2016
    • Minor grammatical changes and clarifications
  • 2nd December, 2016
    • Fixed bug in Callback::operator==()
    • Updated the attached source code

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
I've been a professional software engineer for over 20 years. When not writing code, I enjoy spending time with the family, camping and riding motorcycles around Southern California.

Comments and Discussions

 
GeneralMy vote of 5 Pin
victorsimon28-Apr-21 22:24
victorsimon28-Apr-21 22:24 
QuestionLinux version Pin
Member 1505009225-Jan-21 5:57
Member 1505009225-Jan-21 5:57 
Hello,
has already been written a Linux version of this framework?
Many thanks.
Regards
GeneralMy vote of 5 Pin
Member 1505009225-Jan-21 5:56
Member 1505009225-Jan-21 5:56 
QuestionHow to implement transition action? Pin
morra14-Mar-17 2:06
professionalmorra14-Mar-17 2:06 
AnswerRe: How to implement transition action? Pin
David Lafreniere16-Mar-17 1:21
David Lafreniere16-Mar-17 1:21 
GeneralRe: How to implement transition action? Pin
morra16-Mar-17 3:30
professionalmorra16-Mar-17 3:30 
GeneralMy vote of 5 Pin
Arthur V. Ratz1-Dec-16 15:51
professionalArthur V. Ratz1-Dec-16 15:51 
GeneralRe: My vote of 5 Pin
David Lafreniere2-Dec-16 10:44
David Lafreniere2-Dec-16 10:44 
GeneralRe: My vote of 5 Pin
Arthur V. Ratz2-Dec-16 15:40
professionalArthur V. Ratz2-Dec-16 15:40 
QuestionMy vote of 5 Pin
doug33318-Nov-16 9:46
doug33318-Nov-16 9:46 
AnswerRe: My vote of 5 Pin
David Lafreniere18-Nov-16 11:20
David Lafreniere18-Nov-16 11:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.