Threads: Basic Theory and Libraries: Unit of Resource Ownership
Threads: Basic Theory and Libraries: Unit of Resource Ownership
Threads: Basic Theory and Libraries: Unit of Resource Ownership
Unit of dispatching
- A process is an execution path through one or more programs:
If we treat these two characteristics as being independent (as does modern OS theory):
1
Benefits of Threads vs Processes
If implemented correctly then threads have some advantages of (multi) processes, they
take:
• Less time to create a new thread than a process, because the newly created
thread uses the current process address space.
• Less time to terminate a thread than a process.
• Less time to switch between two threads within the same process, partly
because the newly created thread uses the current process address space.
• Less communication overheads -- communicating between the threads of
one process is simple because the threads share everything: address space, in
particular. So, data produced by one thread is immediately available to all the
other threads.
Single threading
-- When the OS does not recognize the concept of thread
Multithreading
-- When the OS supports multiple threads of execution within a single process
2
Fig. 28.1 Threads and Processes Some example popular OSs and their thread support
are:
MS-DOS
-- support a single user process and a single thread
UNIX
-- supports multiple user processes but only supports one thread per process
Solaris
-- supports multiple threads
3
of as a single, monolithic thread. Multithreaded programs can be more adaptive to
variations in user demands than single threaded programs.
• Use fewer system resources -- Programs that use two or more processes
that access common data through shared memory are applying more than one
thread of control. However, each process has a full address space and operating
systems state. The cost of creating and maintaining this large amount of state
information makes each process much more expensive than a thread in both time
and space. In addition, the inherent separation between processes can require a
major effort by the programmer to communicate between the threads in different
processes, or to synchronize their actions.
Figure 28.2 illustrates different process models and thread control in a single thread and
multithreaded application.
4
Example 2: Matrix Multiplication
Matrix Multiplication essentially involves taking the rows of one matrix and multiplying
and adding corresponding columns in a second matrix i.e:
Fig. 28.3 Matrix Multiplication (3x3 example) Note that each element of the resultant
matrix can be computed independently, that is to say by a different thread.
Thread Levels
There are two broad categories of thread implementation:
There are merits to both, in fact some OSs allow access to both levels (e.g. Solaris).
• The kernel is not aware of thread activity but it is still managing process
activity
• When a thread makes a system call, the whole process will be blocked but
for the thread library that thread is still in the running state
• So thread states are independent of process states
5
Advantages and inconveniences of ULT
Advantages:
Disadvantages:
• Most system calls are blocking and the kernel blocks processes -- So all
threads within the process will be blocked
• The kernel can only assign processes to processors -- Two threads within
the same process cannot run simultaneously on two processors
Advantages
• The kernel can simultaneously schedule many threads of the same process
on many processors blocking is done on a thread level
• Kernel routines can be multithreaded
Disadvantages:
• Thread switching within the same process involves the kernel, e.g if we
have 2 mode switches per thread switch this results in a significant slow down.
Solaris is an example of an OS that combines both ULT and KLT (Figure 28.4:
6
• User-level threads (threads library) invisible to the OS are the interface for
application parallelism
• Kernel threads the unit that can be dispatched on a processor
• Lightweight processes (LWP) each LWP supports one or more ULTs and
maps to exactly one KLT
Threads libraries
The interface to multithreading support is through a subroutine library, libpthread for
POSIX threads, and libthread for Solaris threads. They both contain code for:
7
The POSIX Threads Library:libpthread,
<pthread.h>
Creating a (Default) Thread
Use the function pthread_create() to add a new thread of control to the current
process. It is prototyped by:
When an attribute object is not specified, it is NULL, and the default thread is created
with the following attributes:
• It is unbounded
• It is nondetached
• It has a default stack and stack size
• It inherits the parent's priority
You can also create a default attribute object with pthread_attr_init() function, and
then use this attribute object to create a default thread. See the Section 29.2.
#include <pthread.h>
pthread_attr_t tattr;
pthread_t tid;
extern void *start_routine(void *arg);
void *arg;
int ret;
/* default behavior*/
ret = pthread_create(&tid, NULL, start_routine, arg);
The pthread_create() function is called with attr having the necessary state behavior.
start_routine is the function with which the new thread begins execution. When
start_routine returns, the thread exits with the exit status set to the value returned by
start_routine.
When pthread_create is successful, the ID of the thread created is stored in the location
referred to as tid.
8
Creating a thread using a NULL attribute argument has the same effect as using a default
attribute; both create a default thread. When tattr is initialized, it acquires the default
behavior.
pthread_create() returns a zero and exits when it completes successfully. Any other
returned value indicates that an error occurred.
#include <pthread.h>
pthread_t tid;
int ret;
int status;
/* waiting to join thread "tid" with status */
ret = pthread_join(tid, &status);
/* waiting to join thread "tid" without status */
ret = pthread_join(tid, NULL);
The pthread_join() function blocks the calling thread until the specified thread
terminates. The specified thread must be in the current process and must not be detached.
When status is not NULL, it points to a location that is set to the exit status of the
terminated thread when pthread_join() returns successfully. Multiple threads cannot
wait for the same thread to terminate. If they try to, one thread returns successfully and
the others fail with an error of ESRCH. After pthread_join() returns, any stack storage
associated with the thread can be reclaimed by the application.
The pthread_join() routine takes two arguments, giving you some flexibility in its use.
When you want the caller to wait until a specific thread terminates, supply that thread's
ID as the first argument. If you are interested in the exit code of the defunct thread,
supply the address of an area to receive it. Remember that pthread_join() works only
for target threads that are nondetached. When there is no reason to synchronize with the
termination of a particular thread, then that thread should be detached. Think of a
detached thread as being the thread you use in most instances and reserve nondetached
threads for only those situations that require them.
9
The main thread wants the results of the lookup but has other work to do in the meantime.
So it does those other things and then waits for its helper to complete its job by executing
pthread_join(). An argument, pbe, to the new thread is passed as a stack parameter.
This can be done here because the main thread waits for the spun-off thread to terminate.
In general, though, it is better to malloc() storage from the heap instead of passing an
address to thread stack storage, which can disappear or be reassigned if the thread
terminated.
Detaching a Thread
The function pthread_detach() is an alternative to pthread_join() to reclaim storage
for a thread that is created with a detachstate attribute set to PTHREAD_CREATE_JOINABLE.
It is prototyped by:
#include <pthread.h>
pthread_t tid;
int ret;
/* detach thread tid */
ret = pthread_detach(tid);
10
The pthread_detach() function is used to indicate to the implementation that storage
for the thread tid can be reclaimed when the thread terminates. If tid has not terminated,
pthread_detach() does not cause it to terminate. The effect of multiple
pthread_detach() calls on the same target thread is unspecified.
Thread-specific data is maintained on a per-thread basis. TSD is the only way to define
and refer to data that is private to a thread. Each thread-specific data item is associated
with a key that is global to all threads in the process. Using the key, a thread can access a
pointer (void *) that is maintained per-thread.
pthread_keycreate() is called once for each key before the key is used. There is no
implicit synchronization. Once a key has been created, each thread can bind a value to the
key. The values are specific to the thread and are maintained for each thread
independently. The per-thread binding is deallocated when a thread terminates if the key
was created with a destructor function. pthread_keycreate() is prototyped by:
#include <pthread.h>
pthread_key_t key;
int ret;
/* key create without destructor */
ret = pthread_key_create(&key, NULL);
/* key create with destructor */
ret = pthread_key_create(&key, destructor);
11
non-NULL value associated with that key, the destructor function is called with the current
associated value when the thread exits. The order in which the destructor functions are
called is unspecified.
#include <pthread.h>
pthread_key_t key;
int ret;
/* key previously created */
ret = pthread_key_delete(key);
Once a key has been deleted, any reference to it with the pthread_setspecific() or
pthread_getspecific() call results in the EINVAL error.
12
#include <pthread.h>
pthread_key_t key;
void *value;
int ret;
Note: pthread_setspecific() does not free its storage. If a new binding is set, the
existing binding must be freed; otherwise, a memory leak can occur.
#include <pthread.h>
pthread_key_t key;
void *value;
/* key previously created */
value = pthread_getspecific(key);
body() {
...
while (write(fd, buffer, size) == -1) {
if (errno != EINTR) {
fprintf(mywindow, "%s\n", strerror(errno));
exit(1);
}
}
...
}
13
This code may be executed by any number of threads, but it has references to two global
variables, errno and mywindow, that really should be references to items private to each
thread.
References to errno should get the system error code from the routine called by this
thread, not by some other thread. So, references to errno by one thread refer to a different
storage location than references to errno by other threads. The mywindow variable is
intended to refer to a stdio stream connected to a window that is private to the referring
thread. So, as with errno, references to mywindow by one thread should refer to a
different storage location (and, ultimately, a different window) than references to
mywindow by other threads. The only difference here is that the threads library takes care
of errno, but the programmer must somehow make this work for mywindow. The next
example shows how the references to mywindow work. The preprocessor converts
references to mywindow into invocations of the mywindow procedure. This routine in turn
invokes pthread_getspecific(), passing it the mywindow_key global variable (it really
is a global variable) and an output parameter, win, that receives the identity of this
thread's window.
Turning Global References Into Private References Now consider this code fragment:
thread_key_t mywin_key;
FILE *_mywindow(void) {
FILE *win;
pthread_getspecific(mywin_key, &win);
return(win);
}
#define mywindow _mywindow()
The mywin_key variable identifies a class of variables for which each thread has its own
private copy; that is, these variables are thread-specific data. Each thread calls
make_mywin to initialize its window and to arrange for its instance of mywindow to refer
to it. Once this routine is called, the thread can safely refer to mywindow and, after
mywindow, the thread gets the reference to its private window. So, references to
mywindow behave as if they were direct references to data private to the thread.
void make_mywindow(void) {
FILE **win;
14
static pthread_once_t mykeycreated = PTHREAD_ONCE_INIT;
pthread_once(&mykeycreated, mykeycreate);
win = malloc(sizeof(*win));
create_window(win, ...);
pthread_setspecific(mywindow_key, win);
}
void mykeycreate(void) {
pthread_keycreate(&mywindow_key, free_key);
}
void free_key(void *win) {
free(win);
}
First, get a unique value for the key, mywin_key. This key is used to identify the thread-
specific class of data. So, the first thread to call make_mywin eventually calls
pthread_keycreate(), which assigns to its first argument a unique key. The second
argument is a destructor function that is used to deallocate a thread's instance of this
thread-specific data item once the thread terminates.
The next step is to allocate the storage for the caller's instance of this thread-specific data
item. Having allocated the storage, a call is made to the create_window routine, which
sets up a window for the thread and sets the storage pointed to by win to refer to it.
Finally, a call is made to pthread_setspecific(), which associates the value contained
in win (that is, the location of the storage containing the reference to the window) with
the key. After this, whenever this thread calls pthread_getspecific(), passing the
global key, it gets the value that was associated with this key by this thread when it called
pthread_setspecific(). When a thread terminates, calls are made to the destructor
functions that were set up in pthread_key_create(). Each destructor function is called
only if the terminating thread established a value for the key by calling
pthread_setspecific().
pthread_t pthread_self(void);
#include <pthread.h>
pthread_t tid;
tid = pthread_self();
15
int pthread_equal(pthread_t tid1, pthread_t tid2);
#include <pthread.h>
pthread_t tid1, tid2;
int ret;
ret = pthread_equal(tid1, tid2);
Initializing Threads
Use pthread_once() to call an initialization routine the first time pthread_once() is
called -- Subsequent calls to have no effect. The prototype of this function is:
int sched_yield(void);
#include <sched.h>
int ret;
ret = sched_yield();
16
#include <pthread.h>
pthread_t tid;
int ret;
struct sched_param param;
int priority;
/* sched_priority will be the priority of the thread */
sched_param.sched_priority = priority;
/* only supported policy, others will result in ENOTSUP */
policy = SCHED_OTHER;
/* scheduling parameters of target thread */
ret = pthread_setschedparam(tid, policy, ¶m);
#include <pthread.h>
pthread_t tid;
sched_param param;
int priority;
int policy;
int ret;
/* scheduling parameters of target thread */
ret = pthread_getschedparam (tid, &policy, ¶m);
/* sched_priority contains the priority of the thread */
priority = param.sched_priority;
#include <pthread.h>
#include <signal.h>
int sig;
pthread_t tid;
int ret;
ret = pthread_kill(tid, sig);
17
pthread_kill() sends the signal sig to the thread specified by tid. tid must be a
thread within the same process as the calling thread. The sig argument must be a valid
signal of the same type defined for signal() in < signal.h> (See Chapter 23)
When sig is zero, error checking is performed but no signal is actually sent. This can be
used to check the validity of tid.
This function returns zero after completing successfully. Any other returned value
indicates that an error occurred. When either of the following conditions occurs,
pthread_kill() fails and returns an error value.
#include <pthread.h>
#include <signal.h>
int ret;
sigset_t old, new;
ret = pthread_sigmask(SIG_SETMASK, &new, &old); /* set new mask */
ret = pthread_sigmask(SIG_BLOCK, &new, &old); /* blocking mask */
ret = pthread_sigmask(SIG_UNBLOCK, &new, &old); /* unblocking */
how determines how the signal set is changed. It can have one of the following values:
SIG_SETMASK
-- Replace the current signal mask with new, where new indicates the new signal
mask.
SIG_BLOCK
-- Add new to the current signal mask, where new indicates the set of signals to
block.
SIG_UNBLOCK
-- Delete new from the current signal mask, where new indicates the set of signals
to unblock.
When the value of new is NULL, the value of how is not significant and the signal mask of
the thread is unchanged. So, to inquire about currently blocked signals, assign a NULL
value to the new argument. The old variable points to the space where the previous signal
mask is stored, unless it is NULL.
18
pthread_sigmask() returns a zero when it completes successfully. Any other returned
value indicates that an error occurred. When the following condition occurs,
pthread_sigmask() fails and returns an errro value.
Terminate a Thread
A thread can terminate its execution in the following ways:
• By returning from its first (outermost) procedure, the threads start routine;
see pthread_create()
• By calling pthread_exit(), supplying an exit status
• By termination with POSIX cancel functions; see pthread_cancel()
#include <pthread.h>
int status;
pthread_exit(&status); /* exit with status */
The pthread_exit() function terminates the calling thread. All thread-specific data
bindings are released. If the calling thread is not detached, then the thread's ID and the
exit status specified by status are retained until the thread is waited for (blocked).
Otherwise, status is ignored and the thread's ID can be reclaimed immediately.
and called:
#include <pthread.h>
pthread_t thread;
int ret;
ret = pthread_cancel(thread);
How the cancellation request is treated depends on the state of the target thread. Two
functions,
pthread_cancel() returns zero after completing successfully. Any other returned value
indicates that an error occurred. When the following condition occurs, the function fails
and returns an error value.
19
Solaris Threads: <thread.h>
Solaris have many similarities to POSIX threads. In this section we focus on the Solaris
features that are not found in POSIX threads. Where functionality is virtually the same
for both Solaris threads and for pthreads, (even though the function names or arguments
might differ), only a brief example consisting of the correct include file and the function
prototype is presented. Where return values are not given for the Solaris threads
functions, see the appropriate man pages.
The Solaris threads API and the pthreads API are two solutions to the same problem:
building parallelism into application software. Although each API is complete in itself,
you can safely mix Solaris threads functions and pthread functions in the same program.
The two APIs do not match exactly, however. Solaris threads supports functions that are
not found in pthreads, and pthreads includes functions that are not supported in the
Solaris interface. For those functions that do match, the associated arguments might not,
although the information content is effectively the same.
By combining the two APIs, you can use features not found in one to enhance the other.
Similarly, you can run applications using Solaris threads, exclusively, with applications
using pthreads, exclusively, on the same system.
To use the Solaris threads functions described in this chapter, you must link with the
Solaris threads library -lthread and include the <thread.h> in all programs.
The function thr_suspend() immediately suspends the execution of the thread specified
by a target thread, (tid below). It is prototyped by:
20
A simple example call is as follows:
#include <thread.h>
Note: pthread_t tid as defined in pthreads is the same as thread_t tid in Solaris
threads. tid values can be used interchangeably either by assignment or through the use
of casts.
A suspended thread will not be awakened by a signal. The signal stays pending until the
execution of the thread is resumed by thr_continue().
thr_continue() returns zero after completing successfully. Any other returned value
indicates that an error occurred. When the following condition occurs, thr_continue()
The following code fragment illustrates the use of the function:
By default, Solaris threads attempt to adjust the system execution resources (LWPs) used
to run unbound threads to match the real number of active threads. While the Solaris
threads package cannot make perfect decisions, it at least ensures that the process
continues to make progress. When you have some idea of the number of unbound threads
21
that should be simultaneously active (executing code or system calls), tell the library
through thr_setconcurrency(int new_level). To get the number of threads being
used, use the function thr_getconcurrencyint(void):
#include <thread.h>
int new_level;
int ret;
ret = thr_setconcurrency(new_level);
Readers/Writer Locks
Readers/Writer locks are another unique feature of Solaris threads. They allow
simultaneous read access by many threads while restricting write access to only one
thread at a time.
When any thread holds the lock for reading, other threads can also acquire the lock for
reading but must wait to acquire the lock for writing. If one thread holds the lock for
writing, or is waiting to acquire the lock for writing, other threads must wait to acquire
the lock for either reading or writing. Readers/writer locks are slower than mutexes, but
can improve performance when they protect data that are not frequently written but that
are read by many concurrent threads. Use readers/writer locks to synchronize threads in
this process and other processes by allocating them in memory that is writable and shared
among the cooperating processes (see mmap(2)) and by initializing them for this
behavior. By default, the acquisition order is not defined when multiple threads are
waiting for a readers/writer lock. However, to avoid writer starvation, the Solaris threads
22
package tends to favor writers over readers. Readers/writer locks must be initialized
before use.
The readers/writer lock pointed to by rwlp and to set the lock state to unlocked. type can
be one of the following
USYNC_PROCESS
-- The readers/writer lock can be used to synchronize threads in this process and
other processes.
USYNC_THREAD
-- The readers/writer lock can be used to synchronize threads in this process, only.
rwlock_init() returns zero after completing successfully. Any other returned value
indicates that an error occurred. When any of the following conditions occur, the function
fails and returns the corresponding value to errno.
Multiple threads must not initialize the same readers/writer lock simultaneously.
Readers/writer locks can also be initialized by allocation in zeroed memory, in which
case a type of USYNC_THREAD is assumed. A readers/writer lock must not be reinitialized
while other threads might be using it.
An example code fragment that initialises Readers/Writer Locks with Intraprocess Scope
is as follows:
#include <thread.h>
rwlock_t rwlp;
int ret;
/* to be used within this process only */
ret = rwlock_init(&rwlp, USYNC_THREAD, 0);
Initializing Readers/Writer Locks with Interprocess Scope
#include <thread.h>
rwlock_t rwlp;
int ret;
/* to be used among all processes */
ret = rwlock_init(&rwlp, USYNC_PROCESS, 0);
23
Acquire a Read Lock
To acquire a read lock on the readers/writer lock use the rw_rdlock() function:
The readers/writer lock pointed to by rwlp. When the readers/writer lock is already
locked for writing, the calling thread blocks until the write lock is released. Otherwise,
the read lock is acquired.
rw_rdlock() returns zero after completing successfully. Any other returned value
indicates that an error occurred. When any of the following conditions occur, the function
fails and returns the corresponding value to errno.
rw_wrlock() returns zero after completing successfully. Any other returned value
indicates that an error occurred.
rw_trywrlock() returns zero after completing successfully. Any other returned value
indicates that an error occurred.
rw_unlock() returns zero after completing successfully. Any other returned value
indicates that an error occurred.
24
Destroy Readers/Writer Lock State
The function rwlock_destroy(rwlock_t *rwlp) destroys any state associated with the
readers/writer lock pointed to by rlwp. The space for storing the readers/writer lock is not
freed.
rwlock_destroy() returns zero after completing successfully. Any other returned value
indicates that an error occurred.
The following example uses a bank account analogy to demonstrate readers/writer locks.
While the program could allow multiple threads to have concurrent read-only access to
the account balance, only a single writer is allowed. Note that the get_balance()
function needs the lock to ensure that the addition of the checking and saving balances
occurs atomically.
rwlock_t account_lock;
float checking_balance = 100.0;
float saving_balance = 100.0;
...
rwlock_init(&account_lock, 0, NULL);
...
float
get_balance() {
float bal;
rw_rdlock(&account_lock);
bal = checking_balance + saving_balance;
rw_unlock(&account_lock);
return(bal);
}
void
transfer_checking_to_savings(float amount) {
rw_wrlock(&account_lock);
checking_balance = checking_balance - amount;
saving_balance = saving_balance + amount;
rw_unlock(&account_lock);
}
Create a Thread
The thr_create() routine is one of the most elaborate of all the Solaris threads library
routines.
25
It is prototyped as follows:
Thjis function adds a new thread of control to the current process. Note that the new
thread does not inherit pending signals, but it does inherit priority and signal masks.
stack_base contains the address for the stack that the new thread uses. If stack_base is
NULL then thr_create() allocates a stack for the new thread with at least stac_size
bytes. stack_size Contains the size, in number of bytes, for the stack that the new
thread uses. If stack_size is zero, a default size is used. In most cases, a zero value
works best. If stack_size is not zero, it must be greater than the value returned by
thr_min_stack(void) inquiry function.
There is no general need to allocate stack space for threads. The threads library allocates
one megabyte of virtual memory for each thread's stack with no swap space reserved.
start_routine contains the function with which the new thread begins execution. When
start_routine returns, the thread exits with the exit status set to the value returned by
start_routine
arg can be anything that is described by void, which is typically any 4-byte value.
Anything larger must be passed indirectly by having the argument point to it.
Note that you can supply only one argument. To get your procedure to take multiple
arguments, encode them as one (such as by putting them in a structure).
flags specifies attributes for the created thread. In most cases a zero value works best.
The value in flags is constructed from the bitwise inclusive OR of the following:
THR_SUSPENDED
-- Suspends the new thread and does not execute start_routine until the thread
is started by thr_continue(). Use this to operate on the thread (such as changing
its priority) before you run it. The termination of a detached thread is ignored.
THR_DETACHED
-- Detaches the new thread so that its thread ID and other resources can be reused
as soon as the thread terminates. Set this when you do not want to wait for the
thread to terminate. Note - When there is no explicit synchronization to prevent it,
an unsuspended, detached thread can die and have its thread ID reassigned to
another new thread before its creator returns from thr_create().
THR_BOUND
-- Permanently binds the new thread to an LWP (the new thread is a bound
thread).
THR_NEW_LWP
26
-- Increases the concurrency level for unbound threads by one. The effect is
similar to incrementing concurrency by one with thr_setconcurrency(),
although THR_NEW_LWP does not affect the level set through the
thr_setconcurrency() function. Typically, THR_NEW_LWP adds a new LWP to
the pool of LWPs running unbound threads.
When you specify both THR_BOUND and THR_NEW_LWP, two LWPs are typically
created -- one for the bound thread and another for the pool of LWPs running
unbound threads.
THR_DAEMON
-- Marks the new thread as a daemon. The process exits when all nondaemon
threads exit. Daemon threads do not affect the process exit status and are ignored
when counting the number of thread exits.
A process can exit either by calling exit() or by having every thread in the
process that was not created with the THR_DAEMON flag call thr_exit(). An
application, or a library it calls, can create one or more threads that should be
ignored (not counted) in the decision of whether to exit. The THR_DAEMONl flag
identifies threads that are not counted in the process exit criterion.
new_thread points to a location (when new_thread is not NULL) where the ID of the new
thread is stored when thr_create() is successful. The caller is responsible for supplying
the storage this argument points to. The ID is valid only within the calling process. If you
are not interested in this identifier, supply a zero value to new_thread.
thr_create() returns a zero and exits when it completes successfully. Any other
returned value indicates that an error occurred. When any of the following conditions are
detected, thr_create() fails and returns the corresponding value to errno.
void thr_yield(void) causes the current thread to yield its execution in favor of
another thread with the same or greater priority; otherwise it has no effect. There is no
guarantee that a thread calling thr_yield() will do so.
27
int thr_sigsetmask(int how, const sigset_t *set, sigset_t *oset) to
change or examine the signal mask of the calling thread.
Terminating a Thread
#include <thread.h>
thread_t tid;
thread_t departedid;
int ret;
int status;
/* waiting to join thread "tid" with status */
ret = thr_join(tid, &departedid, (void**)&status);
/* waiting to join thread "tid" without status */
ret = thr_join(tid, &departedid, NULL);
/* waiting to join thread "tid" without return id and status */
ret = thr_join(tid, NULL, NULL);
When the tid is (thread_t) 0, then thread_join() waits for any undetached thread in
the process to terminate. In other words, when no thread identifier is specified, any
undetached thread that exits causes thread_join() to return.
#include <thread.h>
thread_t tid;
thread_t departedid;
int ret;
int status;
/* waiting to join thread "tid" with status */
ret = thr_join(NULL, &departedid, (void **)&status);
By indicating NULL as thread id in the thr_join(), a join will take place when any
non detached thread in the process exits. The departedid will indicate the thread ID of
exiting thread.
Except for the function names and arguments, thread specific data is the same for Solaris
as it is for POSIX.
28
int thr_setspecific(thread_key_t key, void *value) binds value to the thread-
specific data key, key, for the calling thread.
In Solaris threads, if a thread is to be created with a priority other than that of its parent's,
it is created in SUSPEND mode. While suspended, the threads priority is modified using the
int thr_setprio(thread_t tid, int newprio) function call; then it is continued.
An unbound thread is usually scheduled only with respect to other threads in the process
using simple priority levels with no adjustments and no kernel involvement. Its system
priority is usually uniform and is inherited from the creating process.
The function thr_setprio() changes the priority of the thread, specified by tid, within
the current process to the priority specified by newprio.
By default, threads are scheduled based on fixed priorities that range from zero, the least
significant, to the largest integer. The tid will preempt lower priority threads, and will
yield to higher priority threads. For example:
#include <thread.h>
thread_t tid;
int ret;
int newprio = 20;
/* suspended thread creation */
ret = thr_create(NULL, NULL, func, arg, THR_SUSPEND, &tid);
/* set the new priority of suspended child thread */
ret = thr_setprio(tid, newprio);
/* suspended child thread starts executing with new priority */
ret = thr_continue(tid);
Use int thr_getprio(thread_t tid, int *newprio) to get the current priority for the thread.
Each thread inherits a priority from its creator. thr_getprio() stores the current priority,
tid, in the location pointed to by newprio.
Historically, most code has been designed for single-threaded programs. This is
especially true for most of the library routines called from C programs. The following
implicit assumptions were made for single-threaded code:
• When you write into a global variable and then, a moment later, read from
it, what you read is exactly what you just wrote.
• This is also true for nonglobal, static storage.
• You do not need synchronization because there is nothing to synchronize
with.
29
The next few examples discuss some of the problems that arise in multithreaded
programs because of these assumptions, and how you can deal with them.
Traditional, single-threaded C and UNIX have a convention for handling errors detected
in system calls. System calls can return anything as a functional value (for example, write
returns the number of bytes that were transferred). However, the value -1 is reserved to
indicate that something went wrong. So, when a system call returns -1, you know that it
failed.
...
Rather than return the actual error code (which could be confused with normal return
values), the error code is placed into the global variable errno. When the system call
fails, you can look in errno to find out what went wrong.
Now consider what happens in a multithreaded environment when two threads fail at
about the same time, but with different errors.
This global variable approach simply does not work for multithreaded programs. Threads
solve this problem through a conceptually new storage class: thread-specific data.
This storage is similar to global storage in that it can be accessed from any procedure in
which a thread might be running. However, it is private to the thread: when two threads
refer to the thread-specific data location of the same name, they are referring to two
different areas of storage.
So, when using threads, each reference to errno is thread-specific because each thread
has a private copy of errno. This is achieved in this implementation by making errno a
macro that expands to a function call.
30
Preparing for Compilation
The following items are required to compile and link a multithreaded program.
The include file <thread.h>, used with the -lthread library, compiles code that is
upward compatible with earlier releases of the Solaris system. This library contains both
interfaces: those with Solaris semantics and those with POSIX semantics. To call
thr_setconcurrency() with POSIX threads, your program needs to include <thread.h>.
The include file <pthread.h>, used with the -lpthread library, compiles code that is
conformant with the multithreading interfaces defined by the POSIX 1003.1c standard.
For complete POSIX compliance, the define flag _POSIX_C_SOURCE should be set to a
(long) value , as follows:
You can mix Solaris threads and POSIX threads in the same application, by including
both <thread.h> and <pthread.h>, and linking with either the -lthread or -lpthread
library. In mixed use, Solaris semantics prevail when compiling with -D_REENTRANT flag
set and linking with -lthread, whereas POSIX semantics prevail when
For POSIX threads behavior, load the libpthread library. For Solaris threads behavior,
load the libthread library. Some POSIX programmers might want to link with -
lthreadto preserve the Solaris distinction between fork() and fork1(). All that -
lpthread really does is to make fork() behave the same way as the Solaris fork1()
call, and change the behavior of alarm().
31
To use libpthread, specify -lpthread last on the cc command line.
Note: For C++ programs that use threads, use the -mt option, rather than -lthread, to
compile and link your application. The -mt option links with libthread and ensures
proper library linking order. ( Using -lthread might cause your program to crash (core
dump).
The Solaris semaphore routines (see Chapter 30.3) are contained in the libthread
library. By contrast, you link with the -lposix4 library to get the standard POSIX
semaphore routines (See Chapter 25)
32
differently in two successive runs, given identical inputs, because of differences
in the thread scheduling order.
Dave Marshall
1/5/1999
33