A framework combining C++ state machines and multicast asynchronous callbacks.
- C++ State Machine with Threads
- Table of Contents
- Preface
- Introduction
- Self-Test Subsystem
- Asynchronous Callbacks
- Poll Events
- Connecting Callbacks to Event Functions
- User Interface
- Run-Time
- References
- Conclusion
Originally published on CodeProject at: C++ State Machine with Threads
CMake is used to create the build files. CMake is free and open-source software. Windows, Linux and other toolchains are supported. See the CMakeLists.txt file for more information.
- C++ State Machine with Modern Asynchronous Multicast Delegates - by David Lafreniere
- C++ State Machine with Asynchronous Multicast Delegates - by David Lafreniere
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-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:
Figure 1: Self-Test Subsystem Inheritance Diagram
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 is 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
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.
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.
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 client’s 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.
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.
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:
STATE_DEFINE(SelfTest, Completed, NoEventData)
{
SelfTestEngine::InvokeStatusCallback("SelfTest::ST_Completed");
// Generate asychronous completed callback if client is registered
if (CompletedCallback)
CompletedCallback(NoData());
InternalEvent(ST_IDLE);
}
STATE_DEFINE(SelfTest, Failed, NoEventData)
{
SelfTestEngine::InvokeStatusCallback("SelfTest::ST_Failed");
// Generate asychronous 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.
The CentrifugeTest
state machine diagram show 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.
Figure 3: CentrifugeTest State Machine
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.
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.
case WM_USER_TIMER:
Timer::ProcessTimers();
break;
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.
// 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.
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);
}
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:
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:
#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:
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:
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.
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:
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.
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.
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.
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.
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.
void SelfTestEngineCompleteCallback(const NoData& data, void* userData)
{
selfTestEngineCompleted = TRUE;
}
Running the project outputs the following console messages:
Figure 4: Console Output
- State Machine Design in C++ - by David Lafreniere
- Asynchronous Multicast Callbacks with Inter-Thread Messaging - by David Lafreniere
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 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.