A Reentrant VI is a VI that has a separate Data Space allocated for each instance of the VI. Multiple instances of reentrant VIs can execute in parallel without interfering with each other. Non-reentrant VIs have a data space that is shared between multiple calls to the VI.
- Open the VI Properties dialog
- Click on the Category->Execution
- Under Reentrancy radio buttons, select the Shared clone reentrant execution OR Preallocated clone reentrant execution
- Then click OK
The default setting is Non-reentrant execution. When this is set there exists only one data space for the VI and only one call to the VI can execute at a time. Because there is only one data space is why Uninitialized Shift Register work to hold data from one call of the VI to the next for Functional Global Variables.
If Sub.vi non-reentrant and it is currently busing servicing a call from A.vi. If B.vi tries to call Sub.vi while it is busy, then B.vi has to wait until it is free. This can be both a very GOOD thing and a very BAD thing depending on circumstances. It's very GOOD when Sub.vi controls access to something like a serial port, where you only want one part of your program accessing it at a time. It's very BAD when Sub.vi controls something like ALL the serial ports, because you may want B.vi to be able to use one serial port while A.vi is busy using a different serial port. Another very bad circumstance is where B.vi is in a critical loop and A.vi is not, yet because of the contention for Sub.vi, A.vi can end up blocking B.vi.
The setting Shared clone reentrant execution allows parallel execution by allowing clones of the VI which each have their own data space. With this setting, if a clone is not in use it will be reused.
For example: if there is three calls to the VI with this setting.
- Call 1 occurs and Clone 1 of the VI is allocated memory and runs.
- Call 2 occurs. Clone 1 is still running. Clone 2 of the VI is allocated memory and runs.
- Meanwhile, Clone 1 finishes.
- Call 3 occurs and because Clone 1 is free it is used without allocating any new memory.
The number of clones is not set at the beginning and memory is allocated only when all clones are busy and a new clone is needed. Because it reuses clones, the amount of memory is less than with the Preallocated clone reentrant execution setting. However, the trade-off is that memory allocation on-the-fly will introduce jitter.
Also, a Functional Global Variable will not work with this setting because there is no guarantee that the same clone will be called from one call to the next. The data space accessed with the Uninitialized Shift Register is different with each clone.
Preallocated Clone Reentrant Execution
The setting Preallocated clone reentrant execution also allows parallel execution by allowing clones of the VI which each have their own data space. The difference from Shared clone reentrant execution is that Preallocated clone reentrant execution creates a clone with its own data space up front. Clones are not shared between calls, each call has its own clone. It uses more memory but all memory is allocated up front.
With this setting a Functional Global Variable could work, however, not in the original sense. The Functional Global Variable could store data from one call of that specific clone to the next call of that specific clone but data is not shared across different clones.
Reentrancy Compared to other Languages
To expand on this, reentrant means that more than one execution is allowed to take place at the same time. In other languages, it is more a situation than a setting. You never mark a C function as allowing or disallowing reentrancy, it is either safe to do so or a source of bugs. In LabVIEW it is a setting, and many times its setting doesn't affect the correctness of a VI, but in some cases, it can be a source of bugs. It depends on what the VI does.
First is access. With reentrancy turned off, only one call to the SubVI can be active at a time. When the current call finishes, the next one can begin. The SubVI calls queue up while the VI is busy. For functions that execute quickly, this is normally fine and reentrancy doesn't affect much.
- If you have a function that uses TCP to talk to another computer and waits for responses, these waits also affect the other SubVI calls that are queued up. So if you have an operation that can occur in parallel and doesn't consume the CPU, you can make the VI reentrant and the multiple subVI calls don't enter a queue, and multiple VIs can talk TCP and wait for responses at once. This allows the wait time of one subVI to be used as work time in another and increases overall performance.
- Given a VI that reads a Global Variable, modifies it, and writes it back, a reentrant subVI means that more than one subVI call at a time can be modifying the Global Variable. This is a race condition which will cause incorrect answers. Lots of real-world devices also get confused when more than one subVI tries to control them at a time. So when trying to protect a global resource, the one of the tools, and frequently the easiest to use is to simply make sure that the access goes through a non-reentrant VI.
The second attribute is data side-effects. If a VI has unconnected controls or uninitialized shift registers on its diagram, then it remembers some amount of information from call to call. A good example of this is a PID or a filter. Data from previous calls affect the result of the next call. For these sorts of VIs, if they are reentrant, then each call gets its own place to store the previous call's state information. If made non-reentrant, there will be only one storage location for all calls to share, so the data will get all jumbled, likely causing an incorrect answer.