Functional global variable

From LabVIEW Wiki
Jump to: navigation, search

Functional Global Variables (FGVs) are a design pattern used in a VI that allows controlled access to data or resources, often allowing various actions to be performed. Functional Globals most often make use of Uninitialized Shift Registers (USRs) or uninitialized Feedback Nodes as the mechanism to store data. USRs are Shift Registers that do not have a wired input coming into the left side of the loop. A USR stores its data between successive runs of the VI as long as the VI remains in memory.

Alternative Nomenclature

Functional Globals are known by various names among LabVIEW programmers.

  • LV2 globals – because LV2 supported uninitialized shift registers
  • USR globals – since most FGs make use of uninitialized shift registers
  • VIG - "VI Global" as the VI performs the functionality of a global
  • Action Engine

Action Engines (AEs) vs Functional Global Variables (FGVs)

It has been argued that the term Action Engine should be used for this design pattern. The Action Engine (AE) design pattern can be thought of as a machine (Engine) that performs some useful task (Action) often on some data. [1]

FGVs would then refer to a subset of AEs that just allow read/write access to data. Action Engines would refer to the larger set that allow some other defined actions on the data.

The official name for these constructs in NI literature is Functional Global Variable (FGV) but understand that the names could be used interchangeably in other literature.

Structure

The most common form of a Functional Global is a While Loop with a TRUE wired to the stop condition. This forces the loop to terminate after just one execution, meaning the while loop is only used to provide a place to put USRs. A For Loop could also be used with a constant wired to the count terminal. Another method is to use a single-element queue to store the data, though this requires more mouse clicks to implement. The performance of each of these is very close and most likely will vary a little from computer to computer.

Simple Functional Global using a While Loop

In order to specify what action to perform, an enumerated type is often used as an input to the Functional Global. Other methods are possible (Booleans, First Call?, etc.), but an enumerated type provides for a readable, extensible list of actions. Furthermore, saving the enumerated type as a typedef control makes any changes to the list automatically update wherever the Functional Global is used in the code. The ‘action’ input is fed to a case structure which contains the code to implement for each of the defined actions.

For more complex situations, multiple actions can be performed on a single call to the FG. This can be a straightforward state machine, or it can allow for different actions based on the data received or on the existence of resources, etc.

The data in a Functional Global could be of any type, but very often there will be several data elements in a single FG. It is helpful to group the data into a cluster to limit the number of wires entering and leaving the FG. Designing the data structures and how you cluster your data should be done early on, rather than haphazardly growing your clusters as you grow your program.

In order to deal with varying input requirements on the different actions, one can create ‘wrappers’ around the Functional Global that allow for different inputs to be required. This also gives the benefit of being able to search your code for certain actions as you can search for a specific VI, but not a specific enumerated type’s value.

Uses

The primary use of Functional Globals is to provide data sharing across an entire project, both horizontally and vertically. Horizontal sharing means multiple top-level VIs running autonomously can all have access to the same data. Vertical sharing means that subVIs at any depth will have access to the data without requiring explicit passing of parameters through VI connector pane terminals. These are common issues in even small-sized programs.

Another common use is to encapsulate some functionality, especially functionality that is linked to persistent data or resources like files or devices. This encapsulation protects the resource and provides an extensible interface for interacting with the data or resource. The encapsulation also allows you to avoid a plethora of shift registers in a single VI. You can offload the shift-register to the Functional Global, and call it when needed, without worrying about wiring the data through all your case structures.

To get a better idea of what can be done with Functional Globals, here is a list of things that can be done:

  • Load information from a file into a FG, access it anywhere in your program
  • Maintain your system’s configuration parameters in one location.
  • Store a control reference (like a tree control) in a FG, and then define an interface to manage that control. For complex controls like trees and multi-column listboxes this can simplify your top-level code.
  • Timers are well implemented with FGs. You can keep the start time in the shift register, and then check the elapsed time when you call it again.
  • Automatically preallocate an array when the FG is first called, then use “Replace Array Subset” to efficiently manipulate the array.
  • Keep an ‘Undo’ list (stack) that allows you to push changes onto a stack, and pop them off when you need to undo the change.

The list could go on and on – whatever you can dream up!

Race Conditions and Locking

One of the main reasons that Functional Globals are encouraged in lieu of global variables is race conditions. The majority of Functional Globals are, by definition, not reentrant so that different calls to the FG will all refer to the same shift registers and hence the same data or resource. This means that when a VI calls a FG, that FG is locked or reserved for use by the calling VI. Other VIs need to wait until it is done before they can call the FG.

Global variables are reentrant. If you try to write a value to the global in several places in your code, updates to the value are unpredictable with respect to time. This means that changes to the global can be subject to race conditions. This is especially true if you read/write to the global where a race condition could occur between the read and the write of the variable.

The downside for Functional Globals is that the locking that takes place can be a bottleneck in the code if many VIs are trying to access the FG at the same time. The majority of applications will not have a problem, but care must be taken if speed is a concern.

It is also important to note that Functional Globals are not a cure-all for race conditions. If actions on the Functional Global are not atomic (i.e. they require more than one call to the FG) then race conditions are still possible. Race conditions are only eliminated if all updates to the data are done inside the FG, in a single call. Here are a couple ways to still allow race conditions with FGs.

  • Ask for the data from the FG, change it, and then write it back to the FG.
  • Successive modifications, such as {Set data, normalize data, sort data, output max} By the time you sort the data, new data could have been set that hasn’t been normalized.

Benefits

  1. Efficient Memory Usage – Because the VI retains the same data space regardless of it use in the code, only a single call to the memory manager is made. If arrays are stored in the USR, and replace array element primitives used, then the memory space is efficiently used wherever the VI is called.
  2. Reduces problems with race conditions, as discussed above.
  3. Error checking can be done on data writes (Global variables don’t allow this).
  4. High-level code more readable – By moving shift registers and code logic into Functional Globals with readable action lists, top-level code become simpler and cleaner.
  5. Reuse. Many FGs are generic enough to be used over and over again.

Concerns

Busy connector panes

Terminals get used up quickly, so be sure to design well up front and choose a connector pane with more terminals than you need. You can avoid excessive terminal usage by grouping all of your data into a cluster (preferably, a Type Definition) that is passed in and out of your functional global which will allow you to bundle and unbundle specific variables as needed on your main VI.

Data Persistence

Data in a USR, and hence in a Functional Global, disappears if the owning VI goes out of memory. When a VI runs for the first time, all of its subVIs that are not already in memory are loaded into memory and they belong to that parent VI. If that parent VI is stopped, all of the subVIs that belong to it are also released from memory – even if there are other VIs in memory still referencing those subVIs. The next time one of those subVIs is run, it is loaded into memory again and that calling VI becomes the new parent VI. But if there was anything stored in USRs, that data is now gone.

This behavior is different from a queue reference, for instance, where the queue is only destroyed when ALL VIs referencing it leave memory. It means that if you use Functional Globals across multiple top-level VIs, then you need to have a VI that is run first and contains all of your Functional Globals as subVIs. This ‘First Run’ VI must stay in memory until all other top-level VIs are closed to insure that the Functional Global data will be available.

Performance

Some testing has shown that, as of LabVIEW 8.5, using a Case Structure performs better (from 1.5x to 3x) than a Select function for controlling the data flow of the Set/Write vs. the Get/Read operation.

Reentrant functional globals

Unshared data

There are certain problems where you do not need to share the persistent data elsewhere in the code. If you create a windowed average VI, it can be made reentrant and can be used in multiple places in your code to average data streams. This is an example of an in-place operation that is not shared, but requires persistent data. Timing a process in a single loop could be done with one instance of a Functional Global. Making this timing FG reentrant allows the developer to use that same timing FG to time other processes. See Reentrant VI for more information.

By reference

It’s possible to make use of reentrancy to duplicate functional globals. This allows to create as many globals as you need (even at run time) without duplicating code or modifying the original FG. The main disadvantage of this technique is the necessity to keep a reference to every instance of the functional global and pass them to the VIs or subVIs that will use them.

With this method, the only change to the original FG is to make the VI reentrant. Keep in mind that, from now on, this functional global can’t be used in the habitual way, since any instance will retain its own values.

To create an instance of the functional global, open it with the Open VI Reference function (use option 0x08 to prepare for reentrant run) and use the output reference to set the control values, run the VI and get the results. To facilitate things an avoid mistakes when getting/setting values, may be advisable to use the Call by Reference node. Furthermore, the encapsulation of the FG actions in specific wrappers, as detailed in the Structure section above, is also useful.

See Also

External Links

  • Community Nugget 4/08/2007 Action Engines on NI Discussion Forums (https://forums.ni.com/t5/LabVIEW/Community-Nugget-4-08-2007-Action-Engines/td-p/503801)
  • Retrieved from "https://labviewwiki.org/w/index.php?title=Functional_global_variable&oldid=31171"