Using Labview To Create Multithreaded Vis
Using Labview To Create Multithreaded Vis
Using Labview To Create Multithreaded Vis
Using LabVIEW to Create Multithreaded VIs for Maximum Performance and Reliability
Norma Dorst
Introduction
Modern operating systems, such as Microsoft Windows 2000/NT/9x, Sun Solaris 2, and Linux, provide multithreading technology and the ability to exploit multiprocessor computers. Unfortunately, these technologies and the associated terminology can be confusing to understand and difficult to implement. However, LabVIEW (version 5.0 and later) brings you the advantages of powerful multithreading technology in a simple straightforward way. This application note introduces the concepts of multithreading and explains how you can benefit from multithreading as you build measurement and automation systems. In addition, this application note explains how you can use this technology in LabVIEW without increasing the development time or complexity.
LabVIEW, ni.com, and National Instruments are trademarks of National Instruments Corporation. Product and company names mentioned herein are trademarks or trade names of their respective companies.
341419C-01
July 2000
www.ni.com
Another example where multithreading provides better system reliability is when you perform high-speed data acquisition and display the results. Screen updates are often slow relative to other operations, such as continuous high-speed data acquisition. If you attempt to acquire large amounts of data at high speed in a single-threaded application and display all that data in a graph, the data buffer may overflow because the processor is forced to spend too much time on the screen update. When the data buffer overflows, you lose data. However, in a LabVIEW multithreaded application with the user interface separated on its own thread, the data acquisition task can reside on a different, higher priority thread. In this scenario, the data acquisition and display run independently so the acquisition can run continuously and send data into memory without interruption. The display runs as fast as it can, drawing whatever data it finds in memory at execution time. The acquisition thread preempts the display thread so you do not lose data when the screen updates.
Multitasking in LabVIEW
LabVIEW uses preemptive multithreading on operating systems that offer this feature. LabVIEW also uses cooperative multithreading. Operating systems and processors with preemptive multithreading use a limited number of threads, so in certain cases, these systems return to using cooperative multithreading.
(Windows 2000/NT/9x and Solaris 2) The application is multithreaded. The execution system preemptively multitasks VIs using threads. However, a limited number of threads are available. For highly parallel applications, the execution system uses cooperative multitasking when available threads are busy. Also, the operating system handles preemptive multitasking between the application and other tasks. (Mac OS and Windows 3.1) The application is single-threaded. The execution system cooperatively multitasks VIs using
its own scheduling system. The application also cooperatively multitasks with other applications by periodically yielding a small amount of time.
(HP-UX and Solaris 1) The application is single-threaded. The execution system cooperatively multitasks VIs using its own scheduling system. The operating system handles preemptive multitasking between the application and other tasks.
Synchronous/Blocking Nodes
A few nodes or items on the block diagram are synchronous, meaning they do not multitask with other nodes. In a multithreaded application, they run to completion, and the thread in which they run is monopolized by that task until the task completes. Code Interface Nodes (CINs), DLL calls, and computation functions run synchronously. Most analysis VIs and data acquisition VIs contain CINs and therefore run synchronously. Almost all other nodes are asynchronous. For example, structures, I/O functions, timing functions, and subVIs run asynchronously. The Wait, Wait for Occurrence, Dialog Box, GPIB, and VISA functions and the Serial VIs wait for the task to complete but can do so without holding up the thread. The execution system takes these tasks off the queue until their task is complete. When the task completes, the execution system puts it at the end of the queue. For example, when the user clicks a button on a dialog box the Dialog Box function displays, the execution system puts the task at the end of the queue.
www.ni.com
The single-threaded execution system multitasks by switching back and forth between responding to user interaction and running VIs. The execution system checks to see if any user interface events require handling. If not, the execution system returns to the VI or accepts the next task off the queue. When you click buttons or pull-down menus, the action you perform might take a while to complete because LabVIEW runs VIs in the background. LabVIEW switches back and forth between responding to your interaction with the control or menu and running VIs.
These execution systems provide some rough partitions for VIs that must run independently from other VIs. By default, VIs run in the Standard execution system. The names Instrument I/O and Data Acquisition are suggestions for the type of tasks to place within these execution systems. I/O and data acquisition work in other systems, but you can use these labels to partition the application and understand the organization. Every execution system except User Interface has its own queue. These execution systems are not responsible for managing the user interface. If a VI in one of these queues needs to update a control, the execution system passes responsibility to the User Interface execution system. Also, every execution system except User Interface has two threads responsible for running VIs from the queue. Each thread handles a task. For example, if a VI calls a CIN, the second thread continues to run other VIs within that execution system. Because each execution system has a limited number of threads, tasks remain pending if the threads are busy, just as in a single-threaded application. Although VIs you write run correctly in the Standard execution system, consider using another execution system. For example, if you are developing instrument drivers, you might want to use the Instrument I/O execution system. Even if you use the Standard execution system, the user interface is still separated into its own thread. Any activities conducted in the user interface, such as drawing on the front panel, responding to mouse clicks, and so on, take place without interfering with the execution time of the block diagram code. Likewise, executing a long computational routine does not prevent the user interface from responding to mouse clicks or keyboard data entry. Computers with multiple processors benefit even more from multithreading. On a single-processor computer, the operating system preempts the threads and distributes time to each thread on the processor. On a multiprocessor computer, threads can run simultaneously on the multiple processors so more than one activity can occur at the same time.
The first five priorities are similar in behavior (lowest to highest), but the Subroutine priority has additional characteristics. The following two sections apply to all of these priorities, except the subroutine level.
www.ni.com
Also, if a high priority VI calls a lower priority subVI, that subVI is raised to the same priority level as the caller for the duration of that call. Consequently, you do not need to modify the priority levels of the subVIs that a VI calls to raise the priority level of the subVI.
Subroutines have an additional feature that can help in time-critical applications. If you right-click on a subVI and select Skip Subroutine Call if Busy from the shortcut menu, the execution system skips the call if the subroutine is currently running in another thread. This can help in time-critical loops where the execution system safely skips the operations the subroutine performs, and where you want to avoid the delay of waiting for the subVI to complete. If you skip the execution of a subVI, all outputs of the subVI become the default value for that data type, not the default value for the indicator on the subVI front panel. For example, numeric outputs are zero, string and array outputs are empty, and Boolean parameters are FALSE. If you want to detect if a subroutine ran, make it return TRUE if it ran successfully and FALSE if it did not.
To begin reentrant execution, select FileVI Properties, select Execution in the VI Properties dialog box, and select Reentrant Execution. If you select this option, several other options become unavailable, including the following: Run when opened Suspend when called Auto Handling of Menus at Launch Allow Debugging
8 www.ni.com
These options are dimmed because the subVI must switch between different copies of the data and different execution states with each call, making it impossible to display the current state continuously.
The Get Time In Seconds function reads the current time in seconds, and the Seconds to Date/Time and converts this value to a cluster of time values (year, month, day, hour, minute, second, and day of week). A Bundle function replaces the current hour and minute with values that represent a later time on the same day from the front panel Time To Wake Me cluster control. The Wake-up Time in Seconds function converts the adjusted record back to seconds, and multiplies the difference between the current time in seconds and the future time by 1,000 to obtain milliseconds. The result passes to a Wait function. The Lunch VI and the Break VI use Snooze as a subVI. The Lunch VI, whose front panel and block diagram are shown in the following illustration, waits until noon and displays a front panel to remind the operator to go to lunch. The Break VI displays a front panel to remind the operator to go on break at 10:00 a.m. The Break VI is identical to the Lunch VI, except the display messages are different.
For the Lunch VI and the Break VI to run in parallel, the Snooze VI must be reentrant. Otherwise, if you start the Lunch VI first, the Break VI waits until the Snooze VI wakes up at noon, which is two hours late.
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.
Application Note 114 10 www.ni.com
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.
Semaphores
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 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. A semaphore, also known as a Mutex, is an object you can use to protect access to shared resources. The code where the shared resources are accessed is called a critical section. In general, you want only one task at a time to have access to a critical section protected by a common semaphore. It is possible for semaphores to permit more than one task (up to a predefined limit) access to a critical section. A semaphore remains in memory as long as the top-level VI with which it is associated is not idle. If the top-level VI becomes idle, LabVIEW clears the semaphore from memory. To prevent this, name the semaphore. LabVIEW clears a named semaphore from memory only when the top-level VI with which it is associated is closed. Use the Create Semaphore VI to create a new semaphore. Use the Acquire Semaphore VI to acquire access to a semaphore. Use the Release Semaphore VI to release access to a semaphore. Use the Destroy Semaphore VI to destroy the specified semaphore. The following illustration shows how you can use a semaphore to protect the critical sections. The semaphore was created by entering 1 in the size input of the Create Semaphore VI.
11
Each block diagram that wants to run a critical section must first call the Acquire Semaphore VI. If the semaphore is busy (its size is 0), the VI waits until the semaphore becomes available. When the Acquire Semaphore VI returns false for timed out, indicating that it acquired the semaphore, the block diagram starts executing the false case. When the block diagram finishes with its critical section (Sequence frame), the Acquire Semaphore VI releases the semaphore, permitting another waiting block diagram to resume execution.
// Global buffers to transfer data between threads. #define kNPts 1024 static i16 gAcquireOut[kNPts]; // acquire static double gProcessArr[kNPts]; // process static double gSaveArr[kNPts]; // save // Acquire and Save helper functions. int InitAcquire(void); void FinishAcquire(void); int InitSave(void); void FinishSave(void); // Structure passed to each thread. typedef struct { int kind; HANDLE doneEvent; HANDLE waitEvent; } SyncRec; // List of threads. enum { kAcquireThread, kProcessThread, kDisplayThread, kChildren };
12
www.ni.com
// Thread sychronization process and the actual work procedure. DWORD WINAPI ThreadShell(LPVOID arg); void DoAcquire(void); void DoProcess(void); void DoSave(void); volatile volatile volatile volatile main() { int i, j, k; char buf[256]; DWORD id; HANDLE shellH, evArr[kChildren]; SyncRec kidsEv[kChildren]; printf("Initializing acquire\n"); // Create synchronization events and threads. for (i = 0; i < kChildren; i++) { // Set info for this thread. kidsEv[i].kind = i; evArr[i] = kidsEv[i].doneEvent = CreateEvent(NULL, FALSE, FALSE, NULL); kidsEv[i].waitEvent = CreateEvent(NULL, FALSE, FALSE, NULL); shellH = CreateThread(NULL, 0, ThreadShell, &kidsEv[i], 0, &id); if (! (kidsEv[i].doneEvent && kidsEv[i].waitEvent && shellH)) { printf("Couldn't create events and threads\n"); ExitProcess(1); } } if (InitAcquire() && InitSave()) { printf("Starting acquire\n"); for (j = 0; (j < 10) && !gAcqFailed && !gProcessFailed && !gSaveFailed; j++) { // Tell children to stop waiting. for (i = 0; i < kChildren; i++) SetEvent(kidsEv[i].waitEvent); // Wait until all children are done. WaitForMultipleObjects(kChildren, evArr, TRUE, INFINITE); // Main thread coordination goes here... // Copy from process buffer to save buffer. memcpy(gSaveArr, gProcessArr, sizeof(gSaveArr)); // Copy from acquire buffer to process buffer. for (k = 0; k < kNPts; k++) gProcessArr[k] = (double) gAcquireOut[k]; } printf("Acquire finished\n"); } // Tell children to stop executing. gExitThreads = TRUE; // Release children from wait. BOOL gExitThreads = FALSE; int gAcqFailed = 0; // set to TRUE if the acquisition failed int gProcessFailed = 0; // set to TRUE if the process failed int gSaveFailed = 0; // set to TRUE if the save failed
13
for (i = 0; i < kChildren; i++) SetEvent(kidsEv[i].waitEvent); // Clean up. FinishAcquire(); FinishSave(); // Do (minimal) error reporting. if (gAcqFailed) printf("Acquire of data failed\n"); if (gProcessFailed) printf("Processing of data failed\n"); if (gSaveFailed) printf("Saving data failed\n"); // Acknowledge finish. printf("Cleanup finished. gets(buf); return 0; } /* A shell for each thread to handle all the event sychronization. Each thread knows what to do by the kind field in SyncRec structure. */ DWORD WINAPI ThreadShell(LPVOID arg) { SyncRec *ev = (SyncRec *) arg; DWORD res; while (1) { // Wait for main thread to tell us to go. res = WaitForSingleObject(ev->waitEvent, INFINITE); if (gExitThreads) break; // Call work procedure. switch (ev->kind) { case kAcquireThread: DoAcquire();break; case kProcessThread: DoProcess();break; case kDisplayThread: DoSave();break; default: printf("Unknown thread kind!\n"); ExitProcess(2); } // Let main thread know we're done. SetEvent(ev->doneEvent); } return 0; } // DAQ Section --------------------------------------------------#define kBufferSize (2*kNPts) static i16 gAcquireBuffer[kBufferSize] = {0}; static i16 gDevice = 1; static i16 gChan = 1; #define kDBModeON 1 Hit <ret> to end...\n");
14
www.ni.com
int InitAcquire(void) { i16 iStatus = 0; i16 iGain = 1; f64 dSampRate = 1000.0; i16 iSampTB = 0; u16 uSampInt = 0; i32 lTimeout = 180; int result = 1; /* This sets a timeout limit (#Sec * 18ticks/Sec) so that if there is something wrong, the program won't hang on the DAQ_DB_Transfer call. */ iStatus = Timeout_Config(gDevice, lTimeout); result = result && (iStatus >= 0); /* Convert sample rate (S/sec) to appropriate timebase and sample interval values. */ iStatus = DAQ_Rate(dSampRate, kPtsPerSecond, &iSampTB, &uSampInt); result = result && (iStatus >= 0); /* Turn ON software double-buffered mode. */ iStatus = DAQ_DB_Config(gDevice, kDBModeON); result = result && (iStatus >= 0); /* Acquire data indefinitely into circular buffer from a single channel. */ iStatus = DAQ_Start(gDevice, gChan, iGain, gAcquireBuffer, kBufferSize, iSampTB, uSampInt); result = result && (iStatus >= 0); gAcqFailed = !result; return result; } void FinishAcquire(void) { /* CLEANUP - Don't check for errors on purpose. */ (void) DAQ_Clear(gDevice); /* Set DB mode back to initial state. */ (void) DAQ_DB_Config(gDevice, kDBModeOFF); /* Disable timeouts. */ (void) Timeout_Config(gDevice, -1); } void DoAcquire(void) { i16 iStatus = 0;
15
i16 hasStopped = 0; u32 nPtsOut = 0; iStatus = DAQ_DB_Transfer(gDevice, gAcquireOut, &nPtsOut, &hasStopped); gAcqFailed = (iStatus < 0); } // Analysis Section ---------------------------------------------void DoProcess(void) { int err; /* Perform power spectrum on the data. */ err = Spectrum(gProcessArr, kNPts); gProcessFailed = (err != 0); } // Save Section -------------------------------------------------static HANDLE *gSaveFile; /* output file pointer */ /* Initialze save information. Return TRUE if we succeed. */ int InitSave(void) { gSaveFile = CreateFile("data.out", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); gSaveFailed = gSaveFile == INVALID_HANDLE_VALUE; return !gSaveFailed; } void FinishSave(void) { CloseHandle(gSaveFile); } void DoSave(void) { DWORD nWritten; BOOL succeeded; succeeded = WriteFile(gSaveFile, gSaveArr, sizeof(gSaveArr), &nWritten, NULL); if (!succeeded || nWritten != sizeof(gSaveArr)) gSaveFailed = 1; }
16
www.ni.com
The Parallel Process VI, developed using LabVIEW 5.0, contains no additional code for threading because multithreading is built into LabVIEW. All the threads or tasks in the block diagram are synchronized each iteration of the loop. As you add functionality to the LabVIEW VI, LabVIEW handles the thread management automatically. LabVIEW resolves most of the thread management difficulties. Instead of creating and controlling threads, you simply enable multithreading as a preference, without any additional programming. LabVIEW chooses a multithreaded configuration for the application, or you can customize configurations and priorities by selecting FileVI Properties and selecting Execution in the VI Properties dialog box. Priority settings automatically translate to set operating system priorities for the multiple threads. You can choose different thread configurations to optimize for data acquisition, instrument control, or other custom configurations. Experimentation in creating multithreading VIs in LabVIEW sometimes yields the best solution. However, if you use C or other text-based languages, rewriting the application to experiment with different configurations can take too much time and effort for the possible rewards. LabVIEW set in multithreaded execution mode manages threads automatically. With LabVIEW, you do not have to be an expert to write multithreaded applications. However, you can still provide choose custom priorities and configurations if you need more control. Although C users have more low-level direct control of individual threads, they face a more complex set of issues when creating multithreaded applications.
17
Conclusion
Used correctly, multithreading offers numerous benefits including more efficient CPU use, better system reliability, and improved performance on multiprocessor computers. As companies continue to standardize on multithreaded operating systems and symmetric multiprocessing computers, the expectations for performance improvements will continue to grow, forcing you to exploit these powerful technologies. Using LabVIEW, you can start today to maximize performance on multithreaded operating systems and/or multiprocessor computers without increasing either your development time or the complexity of your application. Because the multithreading technology of LabVIEW is implemented transparently, no extra programming is required to take full advantage of multithreading technologies.