Solaris provides the library
libthread.so, which contains the native Solaris thread routines. The header file
thread.h contains the definitions of these routines and the data structures they use.
Creating Threads
The
thr_create function creates a new thread and starts it running (although you can specify that the thread should be created in a suspended state, in which case it will not begin executing until you invoke the
thr_continue function). The
thr_create routine expects a number of parameters; you can specify information such as the size and location of the stack to use, and options such as whether the thread should run as a foreground thread or in the background as a daemon. You are also expected to supply a pointer to a function that the thread will execute, and an optional parameter for this function.
The thread function must take a single
void * parameter, and return a
void *. When the function finishes, the thread will terminate. Each thread is allocated a thread identifier (
thread_t), which you can use to control the thread once it is running. Threads can also be bound or unbound. A bound thread will always be used to execute the function specified by
thr_create. Unbound threads are not tied to a function, and they effectively constitute a thread pool that Solaris can use to execute functions concurrently.
The most common form of
thr_create starts a bound thread running in the foreground using a default stack, as shown in the example in
Listing 1 (the variable
myData is passed as the parameter
arg to
runThread).
Note that, as with most Unix system calls,
thr_create returns the value -1 if an error occurs and the thread cannot be created. Strictly speaking, you should catch and test this return value, reporting an error if problems arise. This paper omits error checking, for the sake of simplicity.
Managing Threads
The Solaris thread library provides a number of routines you can use to control a thread once it is running. For example,
thr_kill will send a signal to a thread, and possibly terminate that thread. (You can only send signals to threads inside the same process.) The function
thr_setprio can change the scheduling priority of a thread. The routine
thr_suspend will temporarily halt a thread, although it can be resumed later using
thr_continue. An executing thread can also voluntarily relinquish the processor by calling
thr_yield.
The reason for using multiple threads is to perform tasks concurrently. Unless you are using a computer that has as many processors as executing threads, however, you are unlikely to achieve true concurrent processing. Instead, Solaris will allocate time slots to threads and run them sequentially.
To further conserve resources, Solaris also optimizes the number of activateable threads inside an application, using an algorithm that ensures that sufficient threads can be made active to allow the process to progress (and also based upon the number of unbound threads available in the thread pool). In other words, although you create a number of threads, Solaris will not necessarily allocate time to all of them. This characteristic might not provide the most effective degree of concurrency for your application.
You can use the
thr_getconcurrency and
thr_setconcurrency methods to ascertain how many concurrent threads Solaris has activated for your program, and change this value if you feel it is too low.
Synchronizing Threads
The Solaris thread library provides the function
thr_join to allow you to synchronize threads. The thread executing
thr_join will wait for a specified thread to finish before continuing. You can also obtain the exit status of the thread (when a thread finishes, the return value of its function is passed back as the exit status). Finally, you can wait for the next thread to finish by specifying a thread id of 0;
thr_join will return the id of the thread.
Listing 2 shows how to wait for a specific thread to exit. The first parameter to
thr_join is the id of the thread to wait for. The second parameter is a pointer to a
thread_t and will be populated with the id of the terminating thread if it is not
NULL (it is possible to wait for one of a group of threads, and the second parameter will indicate which thread finished). The third parameter will be filled in with the exit status of the terminating thread if it is not
NULL.
Controlling Access to Shared Resources
Concurrent threads sharing access to the same resources need careful coordination. The Solaris thread library provides mutexes and condition variables to help you manage threads manipulating shared data. The functions and data structures are located in the
synch.h header file.
A mutex is a mutual exclusion lock designed to prevent two threads from simultaneously executing critical sections of code that read and/or write the same data. You can use mutexes to coordinate threads running in the same or different processes. You can initialize a mutex using
mutex_init. A thread can then attempt to lock the mutex using
mutex_lock. If the mutex is already locked,
mutex_lock will block until it is released.
The function
mutex_unlock will release a mutex (a thread can only release a mutex that it has locked). The
mutex_trylock function will attempt to lock a mutex, but will terminate immediately with an error if the mutex is already locked. The
mutex_destroy routine will remove a mutex.
Mutexes are simple to use, but they are not always sufficient by themselves. For example, consider the following scenario that uses a single mutex to protect a queue of data:
- A writer thread wants to ensure that the queue will not be accessed while it is being written. The thread therefore locks the mutex to guarantee exclusive access to the queue, adds a new item to the queue, and then releases the mutex.
- A reader thread wants to guarantee that the data in the queue will not change while it is being read. The reader therefore locks the mutex to obtain exclusive access to the queue, retrieves the first item from the queue, and then releases the mutex.
If the reader and writer threads both try to lock the mutex at the same time, one of them will be blocked until the other releases the mutex. It is worth considering, however, what happens if the reader thread locks the mutex and the queue is empty. In many implementations, the reader will wait until an item is available before continuing. It will have to release the mutex first; otherwise the writer thread will be blocked and unable to write to the queue.
The logic in the reader thread can become very contorted, especially if there are many reader threads all accessing the same queue (you also want to avoid performing a busy-wait in the reader thread, continually locking the mutex, examining the queue, and releasing the lock if it is empty before trying again).
Condition variables are designed to solve this type of problem. A condition variable is associated with a mutex and can send a signal to a thread waiting for that mutex to be released. In the meantime, the waiting thread is held in a suspended state. The code snippets in
Listing 3 and
Listing 4 show how to use a mutex and a condition variable. In Listing 3, the writer thread locks the
queueReady mutex, pushes some data onto the queue, signals the condition variable, and releases the mutex.
In Listing 4, the reader thread locks the mutex. If the mutex is already locked by the writer thread, the reader will block until it becomes available. Once the reader has obtained the mutex, it invokes the
cond_wait function to wait for the writer to signal the
signalQueueReady condition variable. The
cond_wait function automatically releases the
queueReady mutex while the thread is suspended, but regains it again when the
cond_wait method finishes.
This temporary release of the mutex allows the writer to lock it and write to the queue. Once the writer has written data to the queue and signaled the condition variable, the reader resumes and obtains the lock. The reader then extracts the data from the queue and, finally, releases the lock.
The previous discussion assumes that the reader executes first and waits for the writer to signal the condition variable. The
cond_signal function releases a single thread executing
cond_wait; if multiple threads are waiting, only one of them will be released, and if no threads are waiting, the signal is lost. Therefore, if the writer has already signaled the condition variable before the reader calls
cond_wait, it will block indefinitely!
There are other ways in which
cond_wait can terminate — if the thread is sent a Unix signal by another thread or process, for example (we will use this solution to fix the blocking problem). Whatever the reason for
cond_wait finishing, it is always guaranteed that the calling thread will have the corresponding mutex locked so the thread can continue and access the locked data. The following example shows how this is useful.
Solaris Calculator and Printers Example
The program shown in
Listing 5 and
Listing 6 presents an example of a single writer and multiple readers sharing the same data structure. The writer thread executes the
calculator::calcPowersOfTwo method, which calculates powers of two from 2 to 20 and places them on a queue.
On a single-processor machine, it is interesting to observe the different scheduling behavior that occurs when the
thr_yield statement in the
alcPowersOfTwo method is uncommented.
The reader threads execute the
printer::printPowersOfTwo method, which retrieves the values from the queue and prints them together with a string identifying which printer object actually produced the output, as shown in
Listing 7 and
Listing 8.
The printer class iterates through the items on the queue, as there may be more than one value available; the calculator thread may reacquire the
queueReady mutex before the printer does and place another value on the queue.
The test harness (
Listing 9) creates the writer and four reader threads. The test harness waits for the writer thread to finish and then sends
SIGUSR1 to each printer thread. The printer threads will all be blocked waiting for
cond_wait to signal the
signalQueueReady condition. Each printer object catches the signal, which causes
cond_wait to terminate and grants the thread the mutex.
The thread can then iterate through any remaining data in the queue. The printer threads are killed when processing has completed; the program waits for the user to press a key before finishing to allow the signal processing to complete.