1

I frequently use the concept of Active Objects (https://www.state-machine.com/active-object) combined with state machines when designing code. The key idea behind these is that only "events" are fed into (and out of) an object and state machines "react to" these events. This leads to a very different type of design from a sequential program. Whereas a sequential program is designed in a "do this, then this returning some results, then do something with those results, ..." an active object/event-based design is "when event A happens do this and emit event B."

This works great for the vast majority of problems, but I occasionally get stuck when I need to "retrieve" some value from an active object/state machine. Let me illustrate with a simple example.

Suppose I am designing a toaster state machine.

Incoming Events:

  • Set Temperature
  • Insert Bread
  • Remove Bread
  • Start
  • Stop

Outgoing Events:

  • Started
  • Stopped
  • Done

Essentially, the toaster (and any active state machine) is a "black box" which can be interacted with by "dispatching" events to it and responding to events "emitted" by it.

Now, imagine Person A interacts with the toaster by setting the temperature and starting it. This person leaves and Person B steps up to the toaster. Since Person B did not perform the original interactions with the toaster, they have no knowledge of what the toaster is doing. Thus, this person wants some way to "query" the internal state of the toaster to make some decision. For example, what if Person B is very inquisitive and would like to know the internal temperature of the toaster right now.

This third type of interaction, "query", is the very essence of sequential programming (via return values) but seems foreign to event-based design. To me, since a state machine should be a black box that is only recognizable by its interactions to the outside, "getting" a value "returned" from a state machine is essentially breaking into this black box to look at the internal state. That being said, I still find that I need to do it (perhaps this is a design problem).

One solution I have used is the idea of "callbacks." This transforms the synchronous query into an asynchronous one. You are essentially telling the state machine, "I would like some information, let me know via this callback when you have it."

stateMachine.getSomething([](int result){
    //Do something with result
});

While this works, it can result in very complex chains of callbacks and requires a fair amount of dynamic memory allocation making it unfriendly for embedded systems.

Another solution would be the idea of "futures" and "promises." (I apologize if I get the terminology slightly wrong as I have never actually used this pattern.) The "getter" method would "return" a "promise" immediately on which the caller will block waiting for a result. Meanwhile, the asynchronous active state machine will perform the processing necessary to get the result. When finished, it will place the value in the future/promise object allowing the caller to continue. In this way, the caller is written in a traditional, sequential, call-and-return fashion, while the state machine remains unchanged.

auto future = statemachine.getSomething();
auto value = future.get(); //Will block here

The main problem here is the requirement that the state machine must be running within a different thread from the caller otherwise a deadlock will result. In other words, if a state machine were to block on a future from another state machine, these state machines would have to be on different threads. Again, a thread for every object is very resource intensive and not so suitable for embedded systems.

Are there other solutions? I admit that I may be hybridizing sequential and event-based programming in my head, but I fail to think of another solution to this type of problem. I would appreciate resources to any helpful reading on this topic.

5
  • Have you considered not doing "query" at all? Maybe the statemachine just gives feedback about which Events are allowed to be put in (or tell the user when an illegal one is given)? I.e. this is what you mean by breaking up the black box. Do you really need to?
    – sfiss
    Commented Jul 28, 2023 at 16:06
  • @sfiss I agree with you; that is the ideal I am striving for. But, for example, in real code I have a complex state machine for a networking protocol. The protocol has certain "attributes" to configure it. Obviously, there is an event to "set" attributes, but I also want one to "get" attributes. This is the same problem as the toaster but I can't necessarily have the network layer give "feedback" on its attributes. Commented Jul 28, 2023 at 16:12
  • I don't understand the point of dogmatically forbidding direct queries of state attributes. A toaster has a temperature at any point in time, so the temperature can be queried immediately without any need to block the caller or the state machine. What am I missing?
    – Doc Brown
    Commented Jul 28, 2023 at 17:30
  • @DocBrown The only reason this would become a problem is, since an active object operates within its own thread, it is not thread-safe to simply access an internal state variable. A better example would be the case where the result is not available immediately so it can't simple be returned. In my network example, perhaps the "query" requires multiple communications to hardware (which are performed in a non-blocking manner). Commented Jul 28, 2023 at 18:31
  • 1
    "The main problem [with futures] is the requirement that the state machine must be running within a different thread from the caller otherwise a deadlock will result." But that's kind of the point of the active record pattern, that the events are handled on the object's own thread which is not shared. This is also a good argument for futures or for publishing events, and against invoking callbacks. But the ur-OOP approach would be to pass a handle to another object to which an async message can be sent once the result is available (same mechanics as a callback, but different interpretation).
    – amon
    Commented Jul 30, 2023 at 13:34

1 Answer 1

2

Taking your toaster example, I would label what you are doing a "Single Threaded Apartment"

Specifically by only using a single thread to service the toaster object, you effectively get thread safety for free - in there can never be two threads mutating the state since there is only one thread in the apartment. Further unless you explicitly hand back control to the outer (event) loop you don't have to worry about overlapping message processing (typically the code completes the handling of one message before moving on to the next) - to put it another way; reentrancy isn't possible either.

If you allow direct access to the objects state (even if the querying thread is read-only) you break your encapsulation in that the reader thread may see a partial/incorrect state, if the state is being mutated by the primary servicing thread (at the same time).

The solution to this, is that all query operations have to be handled/serviced via the event loop, so that they are made on the primary servicing thread.

This is an old problem (I would suggest reading up on the Microsoft COM model) specifically in COM this would be known as "Inter thread marshaling".

Specifically there are only really two possibilities:

Query from the Event Thread

For various reasons it is possible that the query is coming in on the correct thread (this typically only happens if you have invoked an external callback from the primary event loop thread).

In such cases you don't need to Marshal the call you can just directly access the state **

** - There are a few caveats here, the primary one being that you need to ensure that all places where you provide outbound/callbacks that the state is valid before making the outbound call so that if the external code uses the primary thread to make a query, the state of the Toaster will still be valid.

Query from a different Thread

In this case the a message is placed on the event loop and the external thread is blocked. Once the Toaster completes processing of all previous events it will service the query on the primary thread. It can then pass the result back to the (blocked) querying thread and release the blocked thread.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.