So we now know that Actors are really just object oriented Queued Message Handlers, and we know how to start the message handling loop of the actor and how to message that loop. Now we need to know how to actually get cool stuff done using actors.
This article is part of the Actor Framework Basics series. See the other parts:
- Part 1 – The Background
- Part 2 – The Actor
- Part 3 – Launching and Communicating
- Part 4 – Being Productive With Actors
- Basic Walkthrough – Creating a Logger
Actors are Objects, So Let’s Extend Them!
An actor is implemented as an LVClass. This means that if we want to extend it’s functionality all we need to do is override some methods.
“Classic” OO Extension
Messages for my actors are just method calls on the actor object. I can make those methods dynamic dispatch if I want. Next I can create child versions of that actor that all implement the dynamic dispatch VI differently. This means I can send the same message to different actors and they’ll all handle the message as they see fit.
For example, let’s say I created a “Logger.lvclass” Actor. This actor can handle one message called “Log”. This takes a string input with the string to be logged. So if I created a Dynamic Dispatch Log.vi for my “Logger” actor and create a message for that method. Now I can create a “CSV Logger.lvcass” that extends my Logger Actor and override “Log.vi” with the CSV functionality. That means that when I launched a CSV Logger, anyone can send it a Log Messaged and it’d be logged to a CSV format. I could also create a “TXT Logger.lvclass” that extended my “Logger” Actor handled the Log message differently.
Remember, a well-designed class means any method that you can call on the parent, you can call on the child (the Liskov Substitution Principle). Actors are classes so they follow the same rules.
A lot of times you don’t just want a QMH, you want a Producer Consumer type architecture. This is achieved in AF by overriding the framework method called “Actor core”. Actor core is marked as a “Must call to Parent” VI. This is because the parent is our message handling loop. So if we override it, we can get additional loops running alongside our message handler.
Helper loops are what do the bulk of the work for your actor. Need a user interface for your actor? Put the event handler in a helper loop. Need to read from a DAQ device? Perform the read in the helper loop. Your helper loop will probably need to message your actor core using the “Read Self Enqueuer” method. Your actor core may need to message your helper loop though. It is up to you to decided what communication mechanism is appropriate for this.
Remember though, you started the helper loop, so you’re responsible for stopping it. The framework takes care of stopping the Message Handler loop (which is inside the call to parent Actor core node), but you need to stop your helper loop(s) when it’s time. If you fail to do this, you’ll end up with an actor running in the background that you can’t communicate with. This is often the most frustrating part of working with actors, but once you make the mistake a few times you’ll remember to be on the lookout for it.
Doing Work in Messages vs in the Helper Loop
Your Actor can only handle one message at a time. It will dequeue them in the order they came in (respecting the priority). Each message will call a method on the Actor object. If you were to put code that takes a long time to execute in that message, you will delay the handling of the next message until the slow code is done. This means that any other message (even ones that are higher in priority) won’t be handled promptly. The AF doesn’t make any promises about timing, but in general you’d like to handle messages quickly. To accomplish this, you should put all of the long running code inside a helper loop. Your actor core will tell you helper loop what to do (via all of LabVIEW’s normal interprocess communication tools) and then it will go back to handling messages. Your helper loop will then take the time to actually perform the task. This allows your message handler to be nice and prompt while still allowing your actor to do a lot of work.
For example: We have a file that needs to be processed. It’s a pretty big file that takes several seconds to open, read and analyzed. If we just executed our “Analyze” code in the AF message, then there would be absolutely no way to talk to the actor until the analyze code is done. What if I needed to abort it? It’d be impossible. So instead, I could create a helper loop that would perform the analysis. Now when my actor received the “Analyze” method it would just send the file to the helper loop, then go back to waiting for messages. The helper loop would then start working through he file. Now if my actor core received an “Abort” message it could again message up the helper loop and tell it to stop. I would still need to build in some mechanisms to make this all work, but it would be possible. It is also easier to debug this way.
Relationship Between Helper And Message Handler Loops
One of the main goals for the Actor Framework is to create chunks of code that are loosely coupled (or ideally, not coupled at all) to the rest of your code. The more you can separate loops, the easier it will be to debug your code. This means that each actor will be one thing. It has a small, public interface that other pieces of code can call into. Each actor should be loosely coupled to other actors.
This is not, however, how your helper loops should be related to your message handler loop. The helper loop SHOULD be tightly coupled to your message handler loop. There should be references shared between the two loops, queues that are expected to be flushed at the right time or any other communication scheme you need. This tight coupling between the loops is what will allow you to get so much done so quickly. You, as the designer of your actor, are responsible for making sure you don’t do something dumb that will cause errors, but that is a reasonable thing for you to do since the helper and the message handler loops are parts of one thing: your actor.
Other Overridable Methods
In addition to actor core there are some other overridable methods. See the list below for information about each. Unless otherwise noted these are methods of Actors.
- Executed while your actor is being launched. All of the communication references are valid in this VI.
- Override when you need to initialize something for your actor, a lot of the time you will initialize references in this VI. References created here will have the same lifetime as your actor.
- If you need to send yourself a message before anyone else, send it in this VI
- Wiring an error to “Error Out” will cause your actor to not launch. The code that tried to launch the actor will get this error from the “Error Out” Terminal of the launch actor. Use this to let your caller know if you couldn’t launch. For example, if you failed to open a log file that your actor needs to work.
- DO NOT Launch a nested actor in pre-launch init. This will create a deadlock. Launch nested actors in your actor core.
- Executed when your message handler loop stops running.
- Override when you need to perform some cleanup action at the end of execution. This can also useful for stopping your helper loops.
- Executed when a message returns an error. This method is responsible for determining when the actor’s message handler loop stops.
- Override when you want to implement your own error handling code.
- By default, the actor will stop itself on all errors.
- Note: When a Stop message is received, you’ll get an Error Code 43 in your handle error (a critical error will be code 1608). A lot of times the error handling code will clear all errors. If you do this your actor will never stop. You need to clear all errors, but wire a true to the “Stop Actor” output if the error code is 43 or 1608.
- Executed when a “Last Ack” Message is received. Every time an actor stops execution it will send it’s status up to it’s caller as a “Last Ack”.
- Override if you need to perform an action when a nested actor has stopped.
- The Last Ack message has the following information
- The error information that stopped the actor.
- The Actor’s final value.
- The Actors Caller-To-Actor Enqueuer (this was the Actor’s Self enqueuer)
- Never executed directly by the framework. You have to decide when to run this method.
- It’s possible to change the actor’s type inside a message. This is useful when implementing the State Design Pattern.
- When changing states you may need to copy over some private data to the new state. Override this method to copy over this private data.
- Executed every time a message is received by the actor but before the message is handled.
- Override when you need to perform an action on the method before it is handled.
- This is very rarely overridden, mostly only while debugging.
- Part of Message.lvclass
- Executed when a message is leftover on the message queue after an actor has stopped. This can happen when an actor is stopped before it’s done handling all of the messages in it’s queue.
- Override when you need to perform an action if a message gets dropped. Most messages (messages containing all By-Value fields) do not need any special drop handling. Usually you’ll only need to handle this case when a message is passing off the lifetime of a By-Ref item.
Questions? Ask them on this discussion topic and I’ll try to answer them!