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.
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.
See GitHub for latest source code:
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:
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:
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()
{
StartCallback(NoData());
}
void SelfTestEngine::StartPrivateCallback()
{
BEGIN_TRANSITION_MAP TRANSITION_MAP_ENTRY (ST_START_CENTRIFUGE_TEST) TRANSITION_MAP_ENTRY (CANNOT_HAPPEN) TRANSITION_MAP_ENTRY (CANNOT_HAPPEN) TRANSITION_MAP_ENTRY (EVENT_IGNORED) TRANSITION_MAP_ENTRY (EVENT_IGNORED) 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.
class SelfTestEngine : public SelfTest
{
public:
static AsyncCallback<SelfTestStatus> StatusCallback;
static SelfTestEngine& GetInstance();
void Start();
WorkerThread& GetThread() { return m_thread; }
static void InvokeStatusCallback(std::string msg);
private:
AsyncCallback<> StartCallback;
void StartPrivateCallback();
SelfTestEngine();
void Complete();
CentrifugeTest m_centrifugeTest;
PressureTest m_pressureTest;
WorkerThread m_thread;
enum States
{
ST_START_CENTRIFUGE_TEST = SelfTest::ST_MAX_STATES,
ST_START_PRESSURE_TEST,
ST_MAX_STATES
};
STATE_DECLARE(SelfTestEngine, StartCentrifugeTest, NoEventData)
STATE_DECLARE(SelfTestEngine, StartPressureTest, NoEventData)
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
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);
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");
if (CompletedCallback)
CompletedCallback(NoData());
InternalEvent(ST_IDLE);
}
STATE_DEFINE(SelfTest, Failed, NoEventData)
{
SelfTestEngine::InvokeStatusCallback("SelfTest::ST_Failed");
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.
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.
class Timer
{
public:
static const DWORD MS_PER_TICK;
AsyncCallback<> Expired;
void Start(DWORD timeout);
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;
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.
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 TRANSITION_MAP_ENTRY (EVENT_IGNORED) TRANSITION_MAP_ENTRY (EVENT_IGNORED) TRANSITION_MAP_ENTRY (EVENT_IGNORED) TRANSITION_MAP_ENTRY (EVENT_IGNORED) TRANSITION_MAP_ENTRY (ST_WAIT_FOR_ACCELERATION) TRANSITION_MAP_ENTRY (ST_WAIT_FOR_ACCELERATION) TRANSITION_MAP_ENTRY (ST_WAIT_FOR_DECELERATION) TRANSITION_MAP_ENTRY (ST_WAIT_FOR_DECELERATION) END_TRANSITION_MAP(NULL)
}
STATE_DEFINE(CentrifugeTest, Acceleration, NoEventData)
{
SelfTestEngine::InvokeStatusCallback("CentrifugeTest::ST_Acceleration");
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:
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.
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:
WorkerThread userInterfaceThread("UserInterface");
void SelfTestEngineStatusCallback(const SelfTestStatus& status, void* userData)
{
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.
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.
int main(void)
{
userInterfaceThread.CreateThread();
SelfTestEngine::GetInstance().GetThread().CreateThread();
SelfTestEngine::StatusCallback.Register(&SelfTestEngineStatusCallback, &userInterfaceThread);
SelfTestEngine::GetInstance().CompletedCallback.Register
(&SelfTestEngineCompleteCallback, &userInterfaceThread);
ThreadWin::StartAllThreads();
SelfTestEngine::GetInstance().Start();
while (!selfTestEngineCompleted)
Sleep(10);
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)
{
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
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
- 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