Skip Navigation Links | |
Exit Print View | |
![]() |
Multithreaded Programming Guide Oracle Solaris 11 Express 11/10 |
1. Covering Multithreading Basics
4. Programming with Synchronization Objects
5. Programming With the Solaris Software
Forking Issues in Process Creation
Fork-One Safety Problem and Solution
Process Creation: exec and exit Issues
Profiling a Multithreaded Program
Nonlocal Goto: setjmp and longjmp
Setting the Thread's Signal Mask
Sending a Signal to a Specific Thread
Waiting for a Specified Signal
Waiting for Specified Signal Within a Given Time
I/O as a Remote Procedure Call
Waiting for I/O Operation to Complete
Shared I/O and New I/O System Calls
6. Programming With Solaris Threads
The traditional UNIX signal model is extended to threads in a fairly natural way. The key characteristics are that the signal disposition is process-wide, but the signal mask is per-thread. The process-wide disposition of signals is established using the traditional mechanisms signal(3C), sigaction(2), and so on.
When a signal handler is marked SIG_DFL or SIG_IGN, the action on receipt of a signal is performed on the entire receiving process. These signals include exit, core dump, stop, continue, and ignore. The action on receipt of these signals is carried out on all threads in the process. Therefore, the issue of which thread picks the signal is nonexistent. The exit, core dump, stop, continue, and ignore signals have no handlers. See the signal.h(3HEAD) man page for basic information about signals.
Each thread has its own signal mask. The signal mask lets a thread block some signals while the thread uses memory or another state that is also used by a signal handler. All threads in a process share the set of signal handlers that are set up by sigaction(2) and its variants.
A thread in one process cannot send a signal to a specific thread in another process. A signal sent by kill(2), sigsend(2), or sigqueue(3RT) to a process is handled by any receptive threads in the process.
Signals are divided into the following categories: traps, exceptions, and interrupts. Traps and exceptions are synchronously generated signals. Interrupts are asynchronously generated signals.
As in traditional UNIX, if a signal is pending, additional occurrences of that signal normally have no additional effect. A pending signal is represented by a bit, not by a counter. However, signals that are posted through the sigqueue(3RT) interface allow multiple instances of the same signal to be queued to the process.
As is the case with single-threaded processes, when a thread receives a signal while blocked in a system call, the thread might return early. When a thread returns early, the thread either returns an EINTR error code, or, in the case of I/O calls, with fewer bytes transferred than requested.
Of particular importance to multithreaded programs is the effect of signals on pthread_cond_wait(3C). This call usually returns without error, a return value of zero, only in response to a pthread_cond_signal(3C) or a pthread_cond_broadcast(3C). However, if the waiting thread receives a traditional UNIX signal, pthread_cond_wait() returns with a return value of zero even though the wakeup was spurious.
Traps, such as SIGILL, SIGFPE, and SIGSEGV, result from an operation on the thread, such as dividing by zero or making reference to nonexistent memory. A trap is handled only by the thread that caused the trap. Several threads in a process can generate and handle the same type of trap simultaneously.
The idea of signals to individual threads is easily extended for synchronously generated signals. The handler is invoked on the thread that generated the synchronous signal.
However, if the process chooses not to establish an appropriate signal handler, the default action is taken when a trap occurs. The default action occurs even if the offending thread is blocked on the generated signal. The default action for such signals is to terminate the process, perhaps with a core dump.
Such a synchronous signal usually means that something is seriously wrong with the whole process, not just with a thread. In this case, terminating the process is often a good choice.
Interrupts, such as SIGINT and SIGIO, are asynchronous with any thread and result from some action outside the process. These interrupts might be signals sent explicitly by another process, or might represent external actions such as a user typing a Control-C.
An interrupt can be handled by any thread whose signal mask allows the interrupt. When more than one thread is able to receive the interrupt, only one thread is chosen.
When multiple occurrences of the same signal are sent to a process, then each occurrence can be handled by a separate thread. However, the available threads must not have the signal masked. When all threads have the signal masked, then the signal is marked pending and the first thread to unmask the signal handles the signal.
Continuation semantics are the traditional way to deal with signals. When a signal handler returns, control resumes where the process was at the time of the interruption. This control resumption is well suited for asynchronous signals in single-threaded processes, as shown in Example 5-1.
This control resumption is also used as the exception-handling mechanism in other programming languages, such as PL/1.
Example 5-1 Continuation Semantics
unsigned int nestcount; unsigned int A(int i, int j) { nestcount++; if (i==0) return(j+1) else if (j==0) return(A(i-1, 1)); else return(A(i-1, A(i, j-1))); } void sig(int i) { printf("nestcount = %d\n", nestcount); } main() { sigset(SIGINT, sig); A(4,4); }
This section describes the operations on signals.
pthread_sigmask(3C) does for a thread what sigprocmask(2) does for a process. pthread_sigmask() sets the thread's signal mask. When a new thread is created, its initial mask is inherited from its creator.
The call to sigprocmask() in a multithreaded process is equivalent to a call to pthread_sigmask(). See the sigprocmask(2) man page for more information.
pthread_kill(3C) is the thread analog of kill(2). A pthread_kill() call sends a signal to a specific thread. A signal that is sent to a specified thread is different from a signal that is sent to a process. When a signal is sent to a process, the signal can be handled by any thread in the process. A signal sent by pthread_kill() can be handled only by the specified thread.
You can use pthread_kill() to send signals only to threads in the current process. Because the thread identifier, type thread_t, is local in scope, you cannot name a thread outside the scope of the current process.
On receipt of a signal by the target thread, the action invoked (handler, SIG_DFL, or SIG_IGN) is global, as usual. If you send SIGXXX to a thread, and SIGXXX to kill a process, the whole process is killed when the target thread receives the signal.
For multithreaded programs, sigwait(2) is the preferred interface to use because sigwait() deals well with asynchronously generated signals.
sigwait() causes the calling thread to wait until any signal identified by the sigwait() function's set argument is delivered to the thread. While the thread is waiting, signals identified by the set argument are unmasked, but the original mask is restored when the call returns.
All signals identified by the set argument must be blocked on all threads, including the calling thread. Otherwise, sigwait() might not work correctly.
Use sigwait() to separate threads from asynchronous signals. You can create one thread that listens for asynchronous signals while you create other threads to block any asynchronous signals set to this process.
The following example shows the syntax of sigwait() .
#include <signal.h> int sigwait(const sigset_t *set, int *sig );
When the signal is delivered, sigwait() clears the pending signal and places the signal number in sig. Many threads can call sigwait() at the same time, but only one thread returns for each signal that is received.
With sigwait(), you can treat asynchronous signals synchronously. A thread that deals with such signals calls sigwait() and returns as soon as a signal arrives. By ensuring that all threads, including the caller of sigwait(), mask asynchronous signals, ensures signals are handled only by the intended handler and are handled safely.
By always masking all signals in all threads and calling sigwait() as necessary, your application is much safer for threads that depend on signals.
Usually, you create one or more threads that call sigwait() to wait for signals. Because sigwait() retrieves even masked signals, be sure to block the signals of interest in all other threads so the signals are not accidentally delivered.
When a signal arrives, a signal-handling thread returns from sigwait() , handles the signal, and calls sigwait() again to wait for more signals. The signal-handling thread is not restricted to using Async-Signal-Safe functions. The signal-handling thread can synchronize with other threads in the usual way. The Async-Signal-Safe category is defined in MT Interface Safety Levels.
Note - sigwait() cannot receive synchronously generated signals.
sigtimedwait(3RT) is similar to sigwait(2) except that sigtimedwait() fails and returns an error when a signal is not received in the indicated amount of time. See the sigtimedwait(3RT) man page for more information.
The UNIX signal mechanism is extended with the idea of thread-directed signals. Thread-directed signals are just like ordinary asynchronous signals, except that thread-directed signals are sent to a particular thread instead of to a process.
A separate thread that waits for asynchronous signals can be safer and easier than installing a signal handler that processes the signals.
A better way to deal with asynchronous signals is to treat these signals synchronously. By calling sigwait(2), a thread can wait until a signal occurs. See Waiting for a Specified Signal.
Example 5-2 Asynchronous Signals and sigwait(2)
main() { sigset_t set; void runA(void); int sig; sigemptyset(&set); sigaddset(&set, SIGINT); pthread_sigmask(SIG_BLOCK, &set, NULL); pthread_create(NULL, 0, runA, NULL, PTHREAD_DETACHED, NULL); while (1) { sigwait(&set, &sig); printf("nestcount = %d\n", nestcount); printf("received signal %d\n", sig); } } void runA() { A(4,4); exit(0); }
This example modifies the code of Example 5-1. The main routine masks the SIGINT signal, creates a child thread that calls function A of the previous example, and issues sigwait() to handle the SIGINT signal.
Note that the signal is masked in the compute thread because the compute thread inherits its signal mask from the main thread. The main thread is protected from SIGINT while, and only while, the thread is not blocked inside of sigwait().
Also, note that no danger exists of having system calls interrupted when you use sigwait().
Another way to deal with signals is with completion semantics.
Use completion semantics when a signal indicates that something so catastrophic has happened that no reason exists to continue executing the current code block. The signal handler runs instead of the remainder of the block that had the problem. In other words, the signal handler completes the block.
In Example 5-3, the block in question is the body of the then part of the if statement. The call to setjmp(3C) saves the current register state of the program in jbuf and returns 0, thereby executing the block.
Example 5-3 Completion Semantics
sigjmp_buf jbuf; void mult_divide(void) { int a, b, c, d; void problem(); sigset(SIGFPE, problem); while (1) { if (sigsetjmp(&jbuf) == 0) { printf("Three numbers, please:\n"); scanf("%d %d %d", &a, &b, &c); d = a*b/c; printf("%d*%d/%d = %d\n", a, b, c, d); } } } void problem(int sig) { printf("Couldn't deal with them, try again\n"); siglongjmp(&jbuf, 1); }
If a SIGFPE floating-point exception occurs, the signal handler is invoked.
The signal handler calls siglongjmp(3C), which restores the register state saved in jbuf, causing the program to return from sigsetjmp() again. The registers that are saved include the program counter and the stack pointer.
This time, however, sigsetjmp(3C) returns the second argument of siglongjmp(), which is 1. Notice that the block is skipped over, only to be executed during the next iteration of the while loop.
You can use sigsetjmp(3C) and siglongjmp(3C) in multithreaded programs. Be careful that a thread never does a siglongjmp() that uses the results of another thread's sigsetjmp().
Also, sigsetjmp() and siglongjmp() restore as well as save the signal mask, but setjmp(3C) and longjmp(3C) do not.
Use sigsetjmp() and siglongjmp() when you work with signal handlers.
Completion semantics are often used to deal with exceptions. In particular, the Sun Ada programming language uses this model.
A concept that is similar to thread safety is Async-Signal safety. Async-Signal-Safe operations are guaranteed not to interfere with operations that are being interrupted.
The problem of Async-Signal safety arises when the actions of a signal handler can interfere with the operation that is being interrupted.
For example, suppose a program is in the middle of a call to printf(3C), and a signal occurs whose handler calls printf(). In this case, the output of the two printf() statements would be intertwined. To avoid the intertwined output, the handler should not directly call printf() when printf() might be interrupted by a signal.
This problem cannot be solved by using synchronization primitives. Any attempt to synchronize between the signal handler and the operation being synchronized would produce an immediate deadlock.
Suppose that printf() is to protect itself by using a mutex. Now, suppose that a thread that is in a call to printf() and so holds the lock on the mutex is interrupted by a signal.
If the handler calls printf(), the thread that holds the lock on the mutex attempts to take the mutex again. Attempting to take the mutex results in an instant deadlock.
To avoid interference between the handler and the operation, ensure that the situation never arises. Perhaps you can mask off signals at critical moments, or invoke only Async-Signal-Safe operations from inside signal handlers.
The only routines that POSIX guarantees to be Async-Signal-Safe are listed in Table 5-2. Any signal handler can safely call in to one of these functions.
Table 5-2 Async-Signal-Safe Functions
|
When an unmasked caught signal is delivered to a thread waiting on a condition variable, when the signal handler returns, the thread returns from the condition wait function with a spurious wakeup: pthread_cond_wait() and pthread_cond_timedwait() return 0 even though no call to pthread_cond_signal() or pthread_cond_broadcast() was made by another thread. Whether SA_RESTART has been specified as a flag to sigaction() has no effect here. The pthread_cond_wait() and pthread_cond_timedwait() functions are not automatically restarted. In all cases, the associated mutex lock is reacquired before returning from the condition wait.
Re-acquisition of the associated mutex lock does not imply that the mutex is locked while the thread is executing the signal handler. The state of the mutex in the signal handler is undefined.