Finite state machines are a tool to achieve certain end. As any tool, they can be abused too.
They are not the most gracious of tools, but the work they are good at is about impossible to achieve by other means (and usually any other approach is then doomed to be a horrible mess thousand times worse than the machine).
The job is operating in conditions where classic wait states are forbidden.
I have to read touchscreen. To read the position, I have to exchange about 15 commands over SPI. I need good 100 readouts a second. I have to wait about 1 microsecond after each command, for respective busy flag to vanish before I can continue. There is also a number of other operations that must be attainable over the same interface, like setting contrast, changing modes, turning backlight on or off, reading temperature. If I performed while(BUSY_BIT);
for each wait, I would eat up all the CPU in matter of moments. If I did sched_yield()
or usleep(1)
, I would never attain the number of readouts I want. The only way is a finite state machine.
But there are ways to make the finite state machine play nice too. Hide the machine behind the scenes and give the developers functions to work with.
My job experience so far was dominated by 2 systems based on 3 different finite state machines.
- a big web portal, where in each step you retrieve some data from the database, and basing on it prepare more queries. In the last step you use the data to generate HTML. Each task - a webpage module - was implemented as a PHP class inheriting from the engine. State was preserved in class variables. Each step was a separate function. At end of a step, stored queries were optimized and sent out to the engine, through caches, and the answers were provided back to the original.
- an embedded device with many subsystems. Task Pump is used. Each module registers a handler that is called many times a second from the main loop. The handler may preserve state in static or class variables, with states. This cooperative multitasking allows for much smaller memory footprint than running all in separate threads, allows for manual prioritizing of tasks by registering them twice, and has the thread run at high priority, overshadowing the rest of the system.
- semi-interpreter. That touchscreen. Function calls and their wait states are registered, but each is called only once, then removed from the program queue. The interpreter is called as a task of taskpump, executing a limited number of functions until encountering a function marked as a wait state (or exceeding the number of functions to be called). Then it continues until the wait state vanishes. Other tasks enqueue jobs as (sometimes long) sequences of functions to be executed, then wait for result. This way I can limit the number of states I need to create to about 4 where I need results. If the command is of "send away, never check result" like "set contrast", they don't need discrete states at all. So the actual states are "wait for event and register requested data", "wait for measurement" and "read results and assign them properly".
The code would be twice as simple and three times clearer if written structurally or sequentially. Except it would't work, or would work with abysmal performance.