Jump to content

Reentrant VI: Difference between revisions

From LabVIEW Wiki
Added detail on the different types of reentrancy.
m Move to Category:VIs
 
(2 intermediate revisions by one other user not shown)
Line 1: Line 1:
{{update}}
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.
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.
 
{{TOCright}}
== Configuration ==
== Configuration ==
To set a [[VI]] to [[Reentrancy|reentrant]]:
To set a [[VI]] to [[Reentrancy|reentrant]]:
Line 11: Line 9:


=== Non-Reentrant Execution ===
=== Non-Reentrant Execution ===
The default setting is '''Non-reentrant execution'''.  When this is set there exists only one [[Data Space|data space]] for the [[VI]] and only one call to the [[VI]] can execute at a time.  Because there is only one [[Data Space|data space]] is why [[Uninitialized Shift Register]] work to hold data from one call of the [[VI]] to the next for [[Functional global variable|Functional Global Variables]]
The default setting is '''Non-reentrant execution'''.  When this is set there exists only one [[Data Space|data space]] for the [[VI]] and only one call to the [[VI]] can execute at a time.  Because there is only one [[Data Space|data space]] is why [[Uninitialized Shift Register]] work to hold data from one call of the [[VI]] to the next for [[Functional global variable|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''.


=== Shared Clone Reentrant Execution ===
=== Shared Clone Reentrant Execution ===
Line 25: Line 25:


Also, a [[Functional global variable|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|data space]] accessed with the [[Uninitialized Shift Register]] is different with each clone.
Also, a [[Functional global variable|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|data space]] accessed with the [[Uninitialized Shift Register]] is different with each clone.
This setting allows for [[Resursion|recursive]] functions, because each clone has it's own [[Data Space|data space]].


=== Preallocated Clone Reentrant Execution ===
=== Preallocated Clone Reentrant Execution ===
Line 31: Line 33:
With this setting a [[Functional global variable|Functional Global Variable]] could work, however, not in the original sense.  The [[Functional global variable|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.
With this setting a [[Functional global variable|Functional Global Variable]] could work, however, not in the original sense.  The [[Functional global variable|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.


==Implementation==
== 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, consider three VIs, ''foo.vi'', ''bar.vi'', and ''sub.vi''. Both ''foo.vi'' and ''bar.vi'' call ''sub.vi''.
 
If ''sub.vi'' is normal (not reentrant), then if ''foo.vi'' tries to call ''sub.vi'' but ''sub.vi'' is busy servicing a call from ''bar.vi'', then ''foo.vi'' has to wait. 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 ''foo.vi'' to be able to use one serial port while ''bar.vi'' is busy using a different serial port. Another very bad circumstance is where ''foo.vi'' is in a critical loop and ''bar.vi'' is not, yet because of the contention for ''sub.vi'', ''bar.vi'' can end up blocking ''foo.vi''.
 
If ''sub.vi'' is reentrant, then both ''foo.vi'' and ''bar.vi'' can call ''sub.vi'' at the same time. In order for this to work, each call to ''sub.vi'' needs to have its own [[Data Space]] which is all the internal storage ''sub.vi'' uses in order to execute its code.
 
Now at this point I need to point out a distinction between [[LabVIEW]] and most other languages. LabVIEW doesn't want to allocate a ''data space'' on the fly, because for LabVIEW that would slow down performance. LabVIEW allocates all the VI data spaces it needs when VIs are being loaded. Except when you use VI Server to call VIs dynamically, all the loading happens before any VIs execute. Therefore, for each place that ''sub.vi'' appears on foo.vi's block diagram, a copy of sub.vi's data space gets embedded in foo.vi's data space, assuming sub.vi is reentrant. If sub.vi isn't reentrant, it just has its one data space allocated that each call will use in turn.
 
In most (all?) other languages, reentrant functions allocate their data spaces on the fly, so there's no storage that goes with each place that a particular function is called.
 
How does LabVIEW's unusual implementation affect us in practical terms? There are really two ways:
 
# If you use uninitialized shift registers to store information, then you can get two different behaviors depending on the reentrancy of your VI. For a non-reentrant VI, you get a data sharing function that lets you move large quantities of information between parallel loops without making copies of it. For a reentrant VI, you get a reusable storage function that can keep independent copies each place you use it.
# The second implication is that you can't do recursion (functions that call themselves) easily in LabVIEW. In most languages, if a function is reentrant then it's OK for it to call itself. In LabVIEW, that would require that the data storage for sub.vi would include a copy of the data storage for sub.vi which would ... to infinity. You can do recursion in LabVIEW if you use VI Server to have a VI call itself dynamically, but as I said, allocating data spaces on the fly is inherently slow. In LabVIEW, it's best to convert recursive algorithms to their iterative equivalents, which I hear is mathematically proven to always be possible. In the iterative version, you'll end up changing the sizes of arrays at each iteration, which is also one of the slower operations in LabVIEW, but is not nearly as slow as dynamic VI calls.
 
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 LV 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.


The setting in LV determines two major attributes about how a VI executes.
== Potential Conditions ==
The setting in [[LabVIEW]] determines two major attributes about how a [[VI]] executes.


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.
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.
* 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.


On the otherhand, given a VI that reads a global modifies it and writes it back, a reentrant subVI means that more than one subVI call at a time can be modifying the global -- 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.
* 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.
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.


[[Category:VI]]
[[Category:VIs]]
[[Category:Intermediate Design Patterns]]
[[Category:Intermediate Design Patterns]]

Latest revision as of 16:49, 7 August 2024

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.

Configuration

To set a VI to reentrant:

  1. Open the VI Properties dialog
  2. Click on the Category->Execution
  3. Under Reentrancy radio buttons, select the Shared clone reentrant execution OR Preallocated clone reentrant execution
  4. Then click OK

Non-Reentrant Execution

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.

Shared Clone Reentrant Execution

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.

This setting allows for recursive functions, because each clone has it's own data space.

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.

Potential Conditions

The setting in LabVIEW determines two major attributes about how a VI executes.

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.