Multithreaded programming is a key approach for taking advantage of multicore processors. By splitting your application into multiple threads, the operating system can balance - or schedule - these threads across multiple processing cores available in the PC. This document discusses the benefits, with respect to multicore processors, that come from using multithread-safe and reentrant functions and drivers in NI's LabVIEW.
In traditional languages, you must break up your program into different threads for parallel execution. Each thread can run at the same time. However, there is a difference between writing code that is safe to run in a multithreaded application, and writing code that executes in parallel to maximize the performance you get from a multicore system. This difference is often illustrated in the drivers or functions that you might use when writing programs. Multithread-safe functions can be called from multiple threads and do not overwrite their data, which prevents conflicts by blocking execution. If one thread is calling the function, any other threads trying to call that function must wait until the first thread is finished. Reentrant functions go a step further by allowing multiple threads to call and execute the same function at the same time, in parallel. Both of these examples execute correctly in a multithreaded program, but you can execute faster with reentrant functions because they run concurrently.
In LabVIEW, the data carried on wires is generally independent of the functions that operate on the data. By definition, the data on a wire is not easily accessed by VIs or functions that are not connected to that wire. If necessary, LabVIEW makes a copy of the data when you split a wire so there are independent versions of the data for subsequent VIs to operate on. In addition, most LabVIEW VIs and drivers are both multithread-safe and reentrant. However, you can set the default configuration of some of the built-in library VIs in LabVIEW to be nonreentrant. Because reentrant VIs use more memory, you may need to make a trade-off between memory usage and parallelism. In many cases, LabVIEW opts toward the memory savings or nonreentrant configuration by default, and lets you decide if the goal is maximum parallelism. If so, the library VIs can easily switch to a reentrant setting.
There are cases and environments in which you may need to use a nonreentrant function or program to prevent problems with accessing functions. Many multithread-safe libraries guarantee their “safety” by locking resources - meaning when one thread calls the function, that function or even the entire library is locked so that no other thread can call it. In a parallel situation, if two different paths of your code, or threads, are attempting to call into the same library or function, the lock forces one of the threads to wait, or block the thread, until the other one completes. Also, permitting only one thread at a time to access a function saves memory space because no extra instances are needed.
However, as mentioned previously, combining parallel programming techniques with reentrancy in your functions can help drive performance in your code.
Because device drivers with LabVIEW (such as NI-DAQmx) are both multithread-safe and reentrant, your function can be called by multiple threads at the same time and still operate correctly without blocking. This is an important feature for writing parallel code and optimizing performance with multicore systems. If you are using code that does not have reentrant execution, this could be why your performance has not increased: your code has to wait until the other threads are done using each function before it can access them. To illustrate this point further, take a look at the VI Hierarchy feature in LabVIEW. To view an individual VI’s hierarchy, select View >> VI Hierarchy. In the VI Hierarchy shown in Figure 1, both F1 and F2 are dependent on the same VI (in this case a very processing-intensive fast Fourier transform algorithm). It is important that this VI is reentrant if F1 and F2 are to execute in parallel.
Figure 1 - VI Hierarchy View in LabVIEW
Reentrancy is an important consideration to eliminate any unnecessary dependencies in your code. Some analysis VIs in LabVIEW are nonreentrant or reentrant by default, so it is important to view the properties of those VIs to ensure they execute in parallel.
To set your LabVIEW VI to be reentrant, select File >> VI Properties, then select Execution from the drop-down menu. You now can check the box next to Reentrant Execution and decide which cloning option you want to choose.
Figure 2 - Reentrant Execution Option in VI Properties Dialog
LabVIEW supports two types of reentrant VIs. Select the Preallocate clone for each instance option if you want to create a clone VI for each call to the reentrant VI before LabVIEW calls the reentrant VI, or if a clone VI must preserve state information across calls. For example, if a reentrant VI contains an uninitialized shift register or a local variable, property, or method that contains values that must remain for future calls to the clone VI, select the Preallocate clone for each instance option. Also select this option if the reentrant VI contains the First Call?function. This is also the recommended setting for VIs running on LabVIEW Real-Time systems, to ensure minimal jitter.
Select the Share clones between instances option to reduce the memory usage associated with preallocating a large number of clone VIs. When you select the Share clones between instances option, LabVIEW does not create the clone VI until a VI makes a call to the reentrant VI. With this option, LabVIEW creates the clone VIs on demand, potentially introducing jitter into the execution of the VI. LabVIEW does not preserve state information across calls to the reentrant VI.
Using drivers that are thread-safe and reentrant is important if you interface with any type of hardware. With these attributes, you can take advantage of multicore technology for performance increases.
With previous versions of LabVIEW, reentrancy was not always a given with device drivers. Traditional NI-DAQ, for example, was multithread-safe in that it would not fail if two different threads called it at the same time. It handled this with a global lock - meaning once a thread called any NI-DAQ function, all other threads would have to wait until that function was complete before they could execute any NI-DAQ functions.
Alternately, NI-DAQmx works much more elegantly in a parallel, multithreaded environment. NI-DAQmx is reentrant –multiple threads can call into the driver simultaneously. You can execute two different analog inputs from two different boards from separate threads within the same program, and they both run simultaneously without blocking. In addition, you can execute an analog input and a digital input from two completely different threads at the same time on the same board. This effectively treats a single hardware resource as two separate resources because of the sophistication of the NI-DAQmx driver.
NI's modular instrument drivers also operate like NI-DAQmx. All of the drivers listed in Table 1 are both thread-safe and reentrant. They all provide the ability to call two of the same functions on two different devices at the same time. This is especially advantageous for large systems with code using multiple instruments.
Table 1 - Thread-Safe and Reentrant Modular Instrument Drivers
When using parallel programming to take advantage of multicore architectures, it’s important to not only think about the programming language and everything involved in writing parallel code, but also to make sure your drivers and functions are appropriate for a parallel environment.