Suggestions for Using Execution Systems and Priorities

In most applications, it is not necessary to use priority levels or an execution system other than the Standard execution system, which automatically handles the multitasking of the VIs. By default, all VIs run in the Standard execution system at Normal priority. In a multithreaded application, a separate thread handles user interface activity, so the VIs are insulated from user interface interaction. In a single-threaded application, the execution system alternates between user interface interaction and execution of the VIs, giving similar results.

In general, the best way to prioritize execution is to use Wait functions to slow down lower priority loops in the application. This is particularly useful in loops for user interface VIs because delays of 100 to 200 ms are barely noticeable to users.

If you use priorities, use them cautiously. If you design higher priority VIs that operate for a while, consider adding waits to those VIs in less time-critical sections of code so they share time with lower priority tasks.

Be careful when manipulating global variables, local variables or other external resources that other tasks change. Use a synchronization technique, such as a functional global variable or a semaphore, to protect access to these resources.

Synchronizing Access to Global and Local Variables and External Resources

Because the execution system can run several tasks in parallel, you must make sure global and local variables and resources are accessed in the proper order.

Preventing Race Conditions

You can prevent race conditions in one of several ways. The simplest way is to have only one place in the entire application through which a global variable is changed.

In a single-threaded application, you can use a Subroutine priority VI to read from or write to a global variable without causing a race condition because a Subroutine priority VI does not share the execution thread with any other VIs. In a multithreaded application, the Subroutine priority level does not guarantee exclusive access to a global variable because another VI running in another thread can access the global variable at the same time.

Functional Global Variables

Functional global variables are non-reentrant VIs that use loops with uninitialized shift registers to hold global data. A functional global variable usually has an action input parameter that specifies which task the VI performs. You can use functional global variables to protect critical sections of code that access the action on a variable, which can help avoid the race conditions associated with global variables. With global variables, when you read the value of a variable, modify that value, and then write the value back to the variable, race conditions can occur because two pieces of parallel code that read the same variable at the same time can overwrite the changes that the other introduces. Use functional global variables to protect actions that modify data. For example, functional global variables can prevent race conditions for critical sections containing actions that read, increment, and write data to memory or actions that update a database or modify a file.

The following illustration shows a functional global variable that implements a simple count global variable. The VI uses an uninitialized shift register in a While Loop to hold the result of the operation. The actions in this example are initialize, read, increment, and decrement.

Every time you call the VI, the block diagram in the loop runs exactly once. Depending on the action parameter, the case inside the loop initializes, does not change, incrementally increases, or incrementally decreases the value of the shift register.

Although you can use functional global variables to implement simple global variables, as shown in the previous example, they are especially useful when implementing more complex data structures, such as a stack or a queue buffer. You also can use functional global variables to protect access to global resources, such as files, instruments, and data acquisition devices, that you cannot represent with a global variable, since calls to the functional global variable execute sequentially unless you mark the VI as reentrant.

You can solve most synchronization problems with functional global variables, because the functional global VI ensures that only one caller at a time changes the data it contains. One disadvantage of functional global variables is that when you want to change the way you modify the resource they hold, you must change the functional global VI block diagram and add a new action. In some applications, where the use of global resources changes frequently, these changes might be inconvenient. In such cases, design the application to use a semaphore to protect access to the global resource.

Data Communication Methods Home

Semaphores

A semaphore, also known as a mutex, is an object you can use to protect access to shared resources, such as global variables. The code that accesses the shared resource is called a critical section. A semaphore protects access to a critical section by allowing only a certain number of tasks to acquire access to the semaphore at the same time. In general, you want only one task at a time to have access to a critical section protected by a common semaphore. However, it is possible to configure a semaphore to permit more than one task (up to a predefined limit) to access a critical section.

Most use cases for semaphores require the following steps:

  1. Identify the critical sections of code�You typically use semaphores to control access to sections of code that access a shared resource. For example, if multiple subVIs interact with the same global variable, you might want to guarantee that those sections of code do not execute simultaneously so you can avoid a race condition. In this example, each section of code that reads from or writes to the global variable is a critical section of code. File and device I/O are other common examples of code that constitutes a critical section.
  2. Create a semaphore for the critical sections to share�Use the Obtain Semaphore Reference VI to create a new semaphore or to retrieve a pre-existing semaphore by name. By default, a new semaphore allows only one task at a time to acquire the semaphore. To allow more tasks to have access to the semaphore at the same time, specify the desired number of tasks for the size input of the Obtain Semaphore Reference VI.
  3. Acquire access to the semaphore before each critical section�Use the Acquire Semaphore VI to request access to the semaphore. If the semaphore is in use by another task, data flow halts at the Acquire Semaphore VI until the other task releases the semaphore. Once a section of code acquires a semaphore, no other task can acquire it until the code releases the semaphore. Therefore, the code guarded between the Acquire Semaphore VI and the Release Semaphore VI is guaranteed to complete execution before another guarded section of code begins executing.
  4. Release access to the semaphore after each critical section�Use the Release Semaphore VI to release access to the semaphore so that another task that is waiting for the semaphore can now acquire it and execute its own critical section of code.
  5. Release the reference to the semaphore�Use the Release Semaphore Reference VI to release a reference to a semaphore. To ensure efficient use of system resources, always release a semaphore reference when you can guarantee that all tasks have finished using the semaphore.
Note��Some advanced uses of semaphores do not acquire and release the semaphore in the same section of code.

Refer to the Simple Semaphore VI in the labview\examples\Synchronization\Semaphore directory for an example of using the Obtain Semaphore Reference VI and the Release Semaphore Reference VI.

Open example� Find related examples

The following illustration shows how you can use a semaphore to protect the critical sections. Another VI created the semaphore and passed the reference to these subVIs. The semaphore has a size of 1, so only one task can access the semaphore at a time.

Each of the previous block diagrams contains a critical section that accesses the same global variable, count. Therefore, the block diagrams share the same semaphore. Before either block diagram executes its critical section, it calls the Acquire Semaphore VI to detect if the other diagram already acquired the semaphore. If the semaphore is busy, the Acquire Semaphore VI waits until the semaphore becomes available. When the semaphore becomes available, the Acquire Semaphore VI returns FALSE for timed out, indicating that the block diagram has permission to execute its critical section. When the block diagram finishes executing its critical section, the Release Semaphore VI releases the semaphore, permitting another waiting block diagram to resume execution.

Lifespan of a Semaphore

A semaphore remains in memory as long as a top-level VI that is not idle owns a reference to the semaphore. If the top-level VI becomes idle, LabVIEW releases all semaphore references the VI owns, including references that exist in subVIs of the top-level VI. If LabVIEW releases the last reference to a named semaphore, LabVIEW destroys the semaphore. Because you can obtain only one reference to an unnamed semaphore, LabVIEW destroys an unnamed semaphore when the top-level VI becomes idle. To use a semaphore across multiple top-level VIs, name the semaphore and call the Obtain Semaphore Reference VI from each top-level VI so that each VI obtains its own unique reference to the semaphore.