A race condition is an event that can occur in electronics or software where the outcome depends on the order that two or more events occur. This can be whichever signal reaches a logic gate first or what software thread changes a stored value.
In the software world, race conditions can only exist in environments that are multi-threaded and where those threads interdependently rely on some shared resource (often a location in memory, but also possibly hardware access).
One of the simplest race conditions can be illustrated with the following LabVIEW code. This code contains two free-spinning loops that read the value from the Race Count indicator on the front panel, add one, then write that incremented value back to the indicator.
In an ideal world, this Race Count variable should track the number of iterations being executed in each of the two loops, but as can be seen from the results, fully 26% of the counts have been lost. Some brief thought will reveal what has happened here. The first loop reads the value and before it can increment and write the value back, the bottom loop also reads that same value. When both write the same incremented value back, one of the counts is lost. We need a way to control access to the value where only one of the loops will access it at a time.
One way to do this is to use timing. The read->increment->write section of the code occurs in a very brief period of time, probably on the order of 1-2 microseconds. Using the following code modification, we can make one loop wait to run only on even millisecond counts and the other to run only on odd millisecond counts.
- You would have to assign different time intervals to every location in the code that was accessing the variable.
- The more access points you have, the slower your code will run. Even in the two case scenario, the execution speed in this admittedly simple example slowed down by a factor of 1000 since we’re waiting a minimum of 1ms per loop now.
- This is still not guaranteed to work, especially on a non-real-time operating system like Windows. All sorts of presumptions can occur in Windows due to hard drive access, antivirus scans, and other applications hogging the CPU, that could easily lead to a millisecond or more of delay between the frames shown above.
A more robust way to prevent race conditions is to use a resource lock, or semaphore. The semaphore is a piece of code that gates access to a resource. When writing your code, you create the semaphore and pass its reference to each loop that may need to access the resource in question. When access is desired, you use the semaphore reference to request access, or “acquire” the semaphore. Whenever the resource is free, the semaphore is granted and your code may proceed to access the resource in question. Once your segment of the code is done, the semaphore must be “released” so that other parts of the code may access the resource in turn. A modification of our example that uses semaphores is shown below.
This takes care of all of the shortcomings from the timing example and guarantees access is only allowed at one location at a time. However, it must be noted that semaphores do not really lock access to a resource. They are something that you need to enforce the use of every time you or anyone on your team accesses a certain race-condition prone area.
Further information on Semaphores in LabVIEW
In its simplest form, the Acquire Semaphore function will simply block and wait for the resource to be available. An optional timeout can be implemented so that your loop may provide alternate functionality if the resource doesn't become available after a certain amount of time has elapsed.
Semaphores can also be used to gate access to a limited resource, such as in a situation where you might have four DAQ cards, but want access from five or more locations. When you obtain the semaphore reference, you can configure it to only block access when there are more than a particular number of outstanding locks by wiring that number to the size input.