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
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
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
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
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
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.