Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/endurodave/asyncstatemachine
Asynchronous State Machine in C++
https://github.com/endurodave/asyncstatemachine
asynchronous-programming cpp cross-platform delegates finite-state-machine state-machine threads
Last synced: about 2 months ago
JSON representation
Asynchronous State Machine in C++
- Host: GitHub
- URL: https://github.com/endurodave/asyncstatemachine
- Owner: endurodave
- License: mit
- Created: 2024-10-28T16:36:52.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2024-11-13T13:46:28.000Z (about 2 months ago)
- Last Synced: 2024-11-13T14:38:18.099Z (about 2 months ago)
- Topics: asynchronous-programming, cpp, cross-platform, delegates, finite-state-machine, state-machine, threads
- Language: C++
- Homepage:
- Size: 254 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Asynchronous State Machine Design in C++
An asynchronous C++ state machine implemented using an asynchronous delegate library.# Table of Contents
- [Asynchronous State Machine Design in C++](#asynchronous-state-machine-design-in-c)
- [Table of Contents](#table-of-contents)
- [Introduction](#introduction)
- [References](#references)
- [CMake Build](#cmake-build)
- [Windows Visual Studio](#windows-visual-studio)
- [Linux Make](#linux-make)
- [Asynchronous Delegates](#asynchronous-delegates)
- [AsyncStateMachine](#asyncstatemachine)
- [Motor Example](#motor-example)
- [Self-Test Subsystem Example](#self-test-subsystem-example)
- [SelfTestEngine](#selftestengine)
- [CentrifugeTest](#centrifugetest)
- [Timer](#timer)
- [Run-Time](#run-time)
- [Conclusion](#conclusion)# 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.
This repository covers how to use a C++ state machines within the context of a multithreaded environment. A new `AsyncStateMachine` class extends the functionality of `StateMachine` documented in [State Machine Design in C++](https://github.com/endurodave/StateMachine). The asynchronous state machine utilizes a C++ `std::thread` for thread support as covered within [C++ std::thread Event Loop](https://github.com/endurodave/StdWorkerThread). Any OS thread is easy adapted. The `std::thread` was convenient for cross-platform Windows and Linux usage.
The `AsyncStateMachine` leverages the asynchronous delegate library documented in [Asynchronous Multicast Delegates in C++](https://github.com/endurodave/AsyncMulticastDelegateModern). In short, the library is capable of targeting any callable function synchronously or asynchronously.
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 state machines.
The state machine and delegate implementations will not be reexplained here. This article focuses on the `AsyncStateMachine` enhancement and integration with the `DelegateLib` library.
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.
## References
* [State Machine Design in C++](https://github.com/endurodave/StateMachine) - A compact C++ finite state machine (FSM) implementation.
* [Asynchronous Multicast Delegates in C++](https://github.com/endurodave/AsyncMulticastDelegateModern) - A C++ delegate library capable of targeting any callable function synchronously or asynchronously.
* [C++ std::thread Event Loop](https://github.com/endurodave/StdWorkerThread) - A worker thread using the C++ thread support library.
# CMake Build
[CMake](https://cmake.org/) is used to create the project build files. See `CMakeLists.txt` for more information.## Windows Visual Studio
`cmake -G "Visual Studio 17 2022" -A Win32 -B ../AsyncStateMachineBuild -S .`
## Linux Make
`cmake -G "Unix Makefiles" -B ../AsyncStateMachineBuild -S .`
# Asynchronous Delegates
If you’re not familiar with a delegate, the concept is quite simple. A delegate can be thought of as a super function pointer. In C++, there's no pointer type capable of pointing to all the possible function variations: instance member, virtual, const, static, lambda, and free (global). A function pointer can’t point to instance member functions, and pointers to member functions have all sorts of limitations. However, delegate classes can, in a type-safe way, point to any function provided the function signature matches. In short, a delegate points to any function with a matching signature to support anonymous function invocation.
Asynchronous delegates take the concept further and permits anonymous invocation of any function on a client specified thread of control. The function and all arguments are safely called from a destination thread simplifying inter-thread communication and eliminating cross-threading errors. The repository [Asynchronous Multicast Delegates in C++](https://github.com/endurodave/AsyncMulticastDelegateModern) covers usage patterns in detail.
The `AsyncStateMachine` uses asynchronous delegates to inject external events into a state machine instance.
# AsyncStateMachine
The `AsyncStateMachine` inherits from `StateMachine`. Create an state machine thread using `CreateThread()` or alternatively attach an existing thread using `SetThread()`.
```cpp
class AsyncStateMachine : public StateMachine
{
public:
/// Constructor.
/// @param[in] maxStates - the maximum number of state machine states.
AsyncStateMachine(BYTE maxStates, BYTE initialState = 0);/// Destructor
virtual ~AsyncStateMachine();/// Create a new thread for this state machine
/// @param[in] threadName - the thread name
void CreateThread(const std::string& threadName);/// Set a thread for this state machine
/// @param[in] thread - a WorkerThread instance
void SetThread(std::shared_ptr thread) { m_thread = thread; }/// Get the thread attached to this state machine
/// @return A WorkerThread instance
std::shared_ptr GetThread() { return m_thread; }protected:
/// @see StateMachine::ExternalEvent()
void ExternalEvent(BYTE newState, const EventData* pData = NULL);private:
// The worker thread instance the state machine executes on
std::shared_ptr m_thread = nullptr;
};
```Converting a state machine from `StateMachine` to `AsyncStateMachine` requires these changes:
1. Inherit from `AsyncStateMachine`.
2. Add `ASYNC_INVOKE()` to all external event functions.
3. Call `CreateThread()` or `SetThread()` to attach a thread.# Motor Example
The `Motor` state machine diagram is shown below.
![Motor State Machine](Motor.png)
`Motor` inherits from `AsyncStateMachine` and implements two external events and four states.
```cpp
#ifndef _MOTOR_H
#define _MOTOR_H#include "AsyncStateMachine.h"
class MotorData : public EventData
{
public:
INT speed;
};// Motor is an asynchronous state machine. All external events are executed
// on the Motor thread of control. The DelegateLib library handles invoking
// functions asynchronously.
class Motor : public AsyncStateMachine
{
public:
Motor();// External events taken by this state machine
void SetSpeed(MotorData* data);
void Halt();private:
INT m_currentSpeed;// State enumeration order must match the order of state method entries
// in the state map.
enum States
{
ST_IDLE,
ST_STOP,
ST_START,
ST_CHANGE_SPEED,
ST_MAX_STATES
};// Define the state machine state functions with event data type
STATE_DECLARE(Motor, Idle, NoEventData)
STATE_DECLARE(Motor, Stop, NoEventData)
STATE_DECLARE(Motor, Start, MotorData)
STATE_DECLARE(Motor, ChangeSpeed, MotorData)// 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(&Stop)
STATE_MAP_ENTRY(&Start)
STATE_MAP_ENTRY(&ChangeSpeed)
END_STATE_MAP
};#endif
```The `Motor::SetSpeed()` external event uses the `ASYNC_INVOKE()` macro to invoke the function on the `Motor` thread of control using the `DelegateLib` library. `ASYNC_INVOKE()` is non-blocking and inserts a message into the `Motor` thread's event queue with pointers to the function and argument, then early returns. Later, the `Motor` thread dequeues the message and calls `Motor::SetSpeed()` on `Motor`'s thread context. The `SetSpeed()` external event function is thread-safe callable by anyone.
```cpp
void Motor::SetSpeed(MotorData* data)
{
/* ASYNC_INVOKE below effectively executes the following code:
// Is this function call executing on this state machine thread?
if (GetThreadId() != WorkerThread::GetCurrentThreadId())
{
// Asynchronously re-invoke the SetSpeed() event on Motor's thread
AsyncInvoke(this, &Motor::SetSpeed, *GetThread(), data);
return;
}*/// Asynchronously invoke Motor::SetSpeed on the Motor thread of control
ASYNC_INVOKE(Motor, SetSpeed, data);BEGIN_TRANSITION_MAP // - Current State -
TRANSITION_MAP_ENTRY (ST_START) // ST_IDLE
TRANSITION_MAP_ENTRY (CANNOT_HAPPEN) // ST_STOP
TRANSITION_MAP_ENTRY (ST_CHANGE_SPEED) // ST_START
TRANSITION_MAP_ENTRY (ST_CHANGE_SPEED) // ST_CHANGE_SPEED
END_TRANSITION_MAP(data)
}
```The `Motor` constructor creates the thread at runtime.
```cpp
Motor::Motor() :
AsyncStateMachine(ST_MAX_STATES),
m_currentSpeed(0)
{
CreateThread("Motor");
}
```# Self-Test Subsystem Example
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:
![Self-Test Subsystem](Figure_1.png)
## 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.
![Self-Test Engine](Figure_2.png)
The `Start` event initiates the self-test engine. `SelfTestEngine::Start()` is an asynchronous function that reinvokes the function on the state machine's execution thread.
```cpp
void SelfTestEngine::Start(const StartData* data)
{
// Asynchronously invoke SelfTestEngine::Start on the SelfTestEngine thread of control
ASYNC_INVOKE(SelfTestEngine, Start, data);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(data)
}
```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.
```cpp
class SelfTestEngine : public SelfTest
{
public:
// Clients register for asynchronous self-test status callbacks
static MulticastDelegateSafe StatusCallback;// Singleton instance of SelfTestEngine
static SelfTestEngine& GetInstance();// Start the self-tests. This is a thread-safe asycnhronous function.
void Start(const StartData* data);static void InvokeStatusCallback(std::string msg);
private:
SelfTestEngine();
void Complete();// Sub self-test state machines
CentrifugeTest m_centrifugeTest;
PressureTest m_pressureTest;StartData m_startData;
// 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, StartData)
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
};
```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.
Note that `m_centrifugeTest` and `m_pressureTest` share the same `SelfTestEngine` thread instance. This means all self-tests execute on the same thread.
```cpp
SelfTestEngine::SelfTestEngine() :
SelfTest("SelfTestEngine", ST_MAX_STATES)
{
// Set owned state machines to execute on SelfTestEngine thread of control
m_centrifugeTest.SetThread(GetThread());
m_pressureTest.SetThread(GetThread());// Register for callbacks when sub self-test state machines complete or fail
m_centrifugeTest.CompletedCallback += MakeDelegate(this, &SelfTestEngine::Complete);
m_centrifugeTest.FailedCallback += MakeDelegate(this, &SelfTest::Cancel);
m_pressureTest.CompletedCallback += MakeDelegate(this, &SelfTestEngine::Complete);
m_pressureTest.FailedCallback += MakeDelegate(this, &SelfTest::Cancel);
}
```The `SelfTest` base class generates the `CompletedCallback` and `FailedCallback` within the `Completed` and `Failed` states respectively as seen below:
```cpp
STATE_DEFINE(SelfTest, Completed, NoEventData)
{
SelfTestEngine::InvokeStatusCallback("SelfTest::ST_Completed");if (CompletedCallback)
CompletedCallback();InternalEvent(ST_IDLE);
}STATE_DEFINE(SelfTest, Failed, NoEventData)
{
SelfTestEngine::InvokeStatusCallback("SelfTest::ST_Failed");if (FailedCallback)
FailedCallback();InternalEvent(ST_IDLE);
}
```One might ask why the state machines use asynchronous delegate 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. `CentrifugeTest` uses state machine inheritance by inheriting the `Idle`, `Completed` and `Failed` states from the `SelfTest` class. The `Timer` class is used to provide `Poll` events via asynchronous delegate callbacks.
![Centrifuge Test State Machine](CentrifugeTest.png)
## 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.
```cpp
class Timer
{
public:
/// Client's register with Expired to get timer callbacks
SinglecastDelegate Expired;/// Constructor
Timer(void);/// Destructor
~Timer(void);/// Starts a timer for callbacks on the specified timeout interval.
/// @param[in] timeout - the timeout in milliseconds.
void Start(std::chrono::milliseconds timeout);/// Stops a timer.
void Stop();/// Gets the enabled state of a timer.
/// @return TRUE if the timer is enabled, FALSE otherwise.
bool Enabled() { return m_enabled; }/// Get the current time in ticks.
/// @return The current time in ticks.
static std::chrono::milliseconds GetTime();// etc...
```## 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.
```cpp
int main(void)
{
try
{
// Create the worker thread
userInterfaceThread.CreateThread();// *** Begin async Motor test ***
Motor motor;auto data = new MotorData();
data->speed = 100;
motor.SetSpeed(data);data = new MotorData();
data->speed = 200;
motor.SetSpeed(data);motor.Halt();
// *** End async Motor test ***// *** Begin async self test ***
// Register for self-test engine callbacks
SelfTestEngine::StatusCallback += MakeDelegate(&SelfTestEngineStatusCallback, userInterfaceThread);
SelfTestEngine::GetInstance().CompletedCallback += MakeDelegate(&SelfTestEngineCompleteCallback, userInterfaceThread);// Start self-test engine
StartData startData;
startData.shortSelfTest = TRUE;
SelfTestEngine::GetInstance().Start(&startData);// Wait for self-test engine to complete
while (!selfTestEngineCompleted)
std::this_thread::sleep_for(std::chrono::milliseconds(10));// Unregister for self-test engine callbacks
SelfTestEngine::StatusCallback -= MakeDelegate(&SelfTestEngineStatusCallback, userInterfaceThread);
SelfTestEngine::GetInstance().CompletedCallback -= MakeDelegate(&SelfTestEngineCompleteCallback, userInterfaceThread);
// *** End async self test **// Exit the worker thread
userInterfaceThread.ExitThread();
}
catch (...)
{
std::cerr << "Exception!" << std::endl;
}return 0;
}
````SelfTestEngine` generates asynchronous callbacks on the `UserInteface` thread. The `SelfTestEngineStatusCallback()` callback outputs the message to the console.
```cpp
void SelfTestEngineStatusCallback(const SelfTestStatus& status)
{
// Output status message to the console "user interface"
cout << status.message.c_str() << endl;
}void SelfTestEngineCompleteCallback()
{
selfTestEngineCompleted = TRUE;
}
```# Conclusion
The `AsyncStateMachine` class allows state machine objects to operate in their own thread context. The `DelegateLib` library offer cross-thread event generation for easy collaboration between state machines.