Chapter 12. POSIX Threads

One method of achieving parallelism is for multiple processes to cooperate and synchronize through shared memory or message passing. An alternative approach uses multiple threads of execution in a single address space. This chapter explains how threads are created, managed and used to solve simple problems. The chapter then presents an overview of basic thread management under the POSIX standard. The chapter discusses different thread models and explains how these models are accommodated under the standard.

A Motivating Problem: Monitoring File Descriptors

A blocking read operation causes the calling process to block until input becomes available. Such blocking creates difficulties when a process expects input from more than one source, since the process has no way of knowing which file descriptor will produce the next input. The multiple file descriptor problem commonly appears in client-server programming because the server expects input from multiple clients. Six general approaches to monitoring multiple file descriptors for input under POSIX are as follows.

  1. A separate process monitors each file descriptor (Program 4.11)

  2. select (Program 4.12 and Program 4.14)

  3. poll (Program 4.17)

  4. Nonblocking I/O with polling (Example 4.39)

  5. POSIX asynchronous I/O (Program 8.14 and Program 8.16)

  6. A separate thread monitors each file descriptor (Section 12.2)

In the separate process approach, the original process forks a child process to handle each file descriptor. This approach works for descriptors representing independent I/O streams, since once forked, the children don’t share any variables. If processing of the descriptors is not independent, the children may use shared memory or message passing to exchange information.

Approaches two and three use blocking calls (select or poll) to explicitly wait for I/O on the descriptors. Once the blocking call returns, the calling program handles each ready file descriptor in turn. The code can be complicated when some of the file descriptors close while others remain open (e.g., Program 4.17). Furthermore, the program can do no useful processing while blocked.

The nonblocking strategy of the fourth approach works well when the program has “useful work” that it can perform between its intermittent checks to see if I/O is available. Unfortunately, most problems are difficult to structure in this way, and the strategy sometimes forces hard-coding of the timing for the I/O check relative to useful work. If the platform changes, the choice may no longer be appropriate. Without very careful programming and a very specific program structure, the nonblocking I/O strategy can lead to busy waiting and inefficient use of processor resources.

POSIX asynchronous I/O can be used with or without signal notification to overlap processing with monitoring of file descriptors. Without signal notification, asynchronous I/O relies on polling as in approach 4. With signal notification, the program does its useful work until it receives a signal advising that the I/O may be ready. The operating system transfers control to a handler to process the I/O. This method requires that the handler use only async-signal-safe functions. The signal handler must synchronize with the rest of the program to access the data, opening the potential for deadlocks and race conditions. Although asynchronous I/O can be tuned very efficiently, the approach is error-prone and difficult to implement.

The final approach uses a separate thread to handle each descriptor, in effect reducing the problem to one of processing a single file descriptor. The threaded code is simpler than the other implementations, and a program can overlap processing with waiting for input in a transparent way.

Threading is not as widely used as it might be because, until recently, threaded programs were not portable. Each vendor provided a proprietary thread library with different calls. The POSIX standard addresses the portability issue with POSIX threads, described in the POSIX:THR Threads Extension. Table E.1 on page 860 lists several additional extensions that relate to the more esoteric aspects of POSIX thread management. Section 12.2 introduces POSIX threads by solving the multiple file descriptor problem. Do not focus on the details of the calls when you first read this section. The remainder of this chapter discusses basic POSIX thread management and use of the library. Chapter 13 explains synchronization and signal handling with POSIX threads. Chapters 14 and 15 discuss the use of semaphores for synchronization. Semaphores are part of the POSIX:SEM Extension and the POSIX:XSI Extension and can be used with threads. Chapters 16 and 17 discuss projects that use threads and synchronization.

Use of Threads to Monitor Multiple File Descriptors

Multiple threads can simplify the problem of monitoring multiple file descriptors because a dedicated thread with relatively simple logic can handle each file descriptor. Threads also make the overlap of I/O and processing transparent to the programmer.

We begin by comparing the execution of a function by a separate thread to the execution of an ordinary function call within the same thread of execution. Figure 12.1 illustrates a call to the processfd function within the same thread of execution. The calling mechanism creates an activation record (usually on the stack) that contains the return address. The thread of execution jumps to processfd when the calling mechanism writes the starting address of processfd in the processor’s program counter. The thread uses the newly created activation record as the environment for execution, creating automatic variables on the stack as part of the record. The thread of execution continues in processfd until reaching a return statement (or the end of the function). The return statement copies the return address that is stored in the activation record into the processor program counter, causing the thread of execution to jump back to the calling program.

Program that makes an ordinary call to processfd has a single thread of execution.

Figure 12.1. Program that makes an ordinary call to processfd has a single thread of execution.

Figure 12.2 illustrates the creation of a separate thread to execute the processfd function. The pthread_create call creates a new “schedulable entity” with its own value of the program counter, its own stack and its own scheduling parameters. The “schedulable entity” (i.e., thread) executes an independent stream of instructions, never returning to the point of the call. The calling program continues to execute concurrently. In contrast, when processfd is called as an ordinary function, the caller’s thread of execution moves through the function code and returns to the point of the call, generating a single thread of execution rather than two separate ones.

Program that creates a new thread to execute processfd has two threads of execution.

Figure 12.2. Program that creates a new thread to execute processfd has two threads of execution.

We now turn to the specific problem of handling multiple file descriptors. The processfd function of Program 12.1 monitors a single file descriptor by calling a blocking read. The function returns when it encounters end-of-file or detects an error. The caller passes the file descriptor as a pointer to void, so processfd can be called either as an ordinary function or as a thread.

The processfd function uses the r_read function of Program 4.3 instead of read to restart reading if the thread is interrupted by a signal. However, we recommend a dedicated thread for signal handling, as explained in Section 13.5. In this case, the thread that executes processfd would have all signals blocked and could call read.

Example 12.1. processfd.c

The processfd function monitors a single file descriptor for input.

#include <stdio.h>
#include "restart.h"
#define BUFSIZE 1024

void docommand(char *cmd, int cmdsize);

void *processfd(void *arg) { /* process commands read from file descriptor */
   char buf[BUFSIZE];
   int fd;
   ssize_t nbytes;

   fd = *((int *)(arg));
   for ( ; ; )  {
      if ((nbytes = r_read(fd, buf, BUFSIZE)) <= 0)
         break;
      docommand(buf, nbytes);
   }
   return NULL;
}

Example 12.1. 

The following code segment calls processfd as an ordinary function. The code assumes that fd is open for reading and passes it by reference to processfd.

void *processfd(void *);
int fd;

processfd(&fd);

Example 12.2. 

The following code segment creates a new thread to run processfd for the open file descriptor fd.

void *processfd(void *arg);

int error;
int fd;
pthread_t tid;

if (error = pthread_create(&tid, NULL, processfd, &fd))
   fprintf(stderr, "Failed to create thread: %s
", strerror(error));

The code of Example 12.1 has a single thread of execution, as illustrated in Figure 12.1. The thread of execution for the calling program traverses the statements in the function and then resumes execution at the statement after the call. Since processfd uses blocking I/O, the program blocks on r_read until input becomes available on the file descriptor. Remember that the thread of execution is really the sequence of statements that the thread executes. The sequence contains no timing information, so the fact that execution blocks on a read call is not directly visible to the caller. The code in Example 12.2 has two threads of execution. A separate thread executes processfd, as illustrated in Figure 12.2.

The function monitorfd of Program 12.2 uses threads to monitor an array of file descriptors. Compare this implementation with those of Program 4.14 and Program 4.17. The threaded version is considerably simpler and takes advantage of parallelism. If docommand causes the calling thread to block for some reason, the thread runtime system schedules another runnable thread. In this way, processing and reading are overlapped in a natural way. In contrast, blocking of docommand in the single-threaded implementation causes the entire process to block.

If monitorfd fails to create thread i, it sets the corresponding thread ID to itself to signify that creation failed. The last loop uses pthread_join, described in Section 12.3, to wait until all threads have completed.

Example 12.2. monitorfd.c

A function to monitor an array of file descriptors, using a separate thread for each descriptor.

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void *processfd(void *arg);

void monitorfd(int fd[], int numfds) {       /* create threads to monitor fds */
   int error, i;
   pthread_t *tid;

   if ((tid = (pthread_t *)calloc(numfds, sizeof(pthread_t))) == NULL) {
      perror("Failed to allocate space for thread IDs");
      return;
   }
   for (i = 0; i < numfds; i++)   /* create a thread for each file descriptor */
      if (error = pthread_create(tid + i, NULL, processfd, (fd + i))) {
         fprintf(stderr, "Failed to create thread %d: %s
",
                         i, strerror(error));
         tid[i] = pthread_self();
      }
   for (i = 0; i < numfds; i++) {
      if (pthread_equal(pthread_self(), tid[i]))
         continue;
      if (error = pthread_join(tid[i], NULL))
         fprintf(stderr, "Failed to join thread %d: %s
", i, strerror(error));
   }
   free(tid);
   return;
}

Thread Management

A thread package usually includes functions for thread creation and thread destruction, scheduling, enforcement of mutual exclusion and conditional waiting. A typical thread package also contains a runtime system to manage threads transparently (i.e., the user is not aware of the runtime system). When a thread is created, the runtime system allocates data structures to hold the thread’s ID, stack and program counter value. The thread’s internal data structure might also contain scheduling and usage information. The threads for a process share the entire address space of that process. They can modify global variables, access open file descriptors, and cooperate or interfere with each other in other ways.

POSIX threads are sometimes called pthreads because all the thread functions start with pthread. Table 12.1 summarizes the basic POSIX thread management functions introduced in this section. The programs listed in Section 12.1 used pthread_create to create threads and pthread_join to wait for threads to complete. Other management functions deal with thread termination, signals and comparison of thread IDs. Section 12.6 introduces the functions related to thread attribute objects, and Chapter 13 covers thread synchronization functions.

Table 12.1. POSIX thread management functions.

POSIX function

description

pthread_cancel

terminate another thread

pthread_create

create a thread

pthread_detach

set thread to release resources

pthread_equal

test two thread IDs for equality

pthread_exit

exit a thread without exiting process

pthread_kill

send a signal to a thread

pthread_join

wait for a thread

pthread_self

find out own thread ID

Most POSIX thread functions return 0 if successful and a nonzero error code if unsuccessful. They do not set errno, so the caller cannot use perror to report errors. Programs can use strerror if the issues of thread safety discussed in Section 12.4 are addressed. The POSIX standard specifically states that none of the POSIX thread functions returns EINTR and that POSIX thread functions do not have to be restarted if interrupted by a signal.

Referencing threads by ID

POSIX threads are referenced by an ID of type pthread_t. A thread can find out its ID by calling pthread_self.

SYNOPSIS

  #include <pthread.h>

  pthread_t pthread_self(void);
                                          POSIX:THR

The pthread_self function returns the thread ID of the calling thread. No errors are defined for pthread_self.

Since pthread_t may be a structure, use pthread_equal to compare thread IDs for equality. The parameters of pthread_equal are the thread IDs to be compared.

SYNOPSIS

   #include <pthread.h>

   pthread_t pthread_equal(thread_t t1, pthread_t t2);
                                                              POSIX:THR

If t1 equals t2, pthread_equal returns a nonzero value. If the thread IDs are not equal, pthread_equal returns 0. No errors are defined for pthread_equal.

Example 12.3. 

In the following code segment, a thread outputs a message if its thread ID is mytid.

pthread_t mytid;

if (pthread_equal(pthread_self(), mytid))
   printf("My thread ID matches mytid
");

Creating a thread

The pthread_create function creates a thread. Unlike some thread facilities, such as those provided by the Java programming language, the POSIX pthread_create automatically makes the thread runnable without requiring a separate start operation. The thread parameter of pthread_create points to the ID of the newly created thread. The attr parameter represents an attribute object that encapsulates the attributes of a thread. If attr is NULL, the new thread has the default attributes. Section 12.6 discusses the setting of thread attributes. The third parameter, start_routine, is the name of a function that the thread calls when it begins execution. The start_routine takes a single parameter specified by arg, a pointer to void. The start_routine returns a pointer to void, which is treated as an exit status by pthread_join.

SYNOPSIS

  #include <pthread.h>

  int pthread_create(pthread_t *restrict thread,
                     const pthread_attr_t *restrict attr,
                     void *(*start_routine)(void *), void *restrict arg);
                                                                             POSIX:THR

If successful, pthread_create returns 0. If unsuccessful, pthread_create returns a nonzero error code. The following table lists the mandatory errors for pthread_create.

error

cause

EAGAIN

system did not have the resources to create the thread, or would exceed system limit on total number of threads in a process

EINVAL

attr parameter is invalid

EPERM

caller does not have the appropriate permissions to set scheduling policy or parameters specified by attr

Do not let the prototype of pthread_create intimidate you—threads are easy to create and use.

Example 12.4. 

The following code segment creates a thread to execute the function processfd after opening the my.dat file for reading.

void *processfd(void *arg);

int error;
int fd;
pthread_t tid;

if ((fd = open("my.dat", O_RDONLY)) == -1)
   perror("Failed to open my.dat");
else if (error = pthread_create(&tid, NULL, processfd, &fd))
   fprintf(stderr, "Failed to create thread: %s
", strerror(error));
else
   printf("Thread created
");

Detaching and joining

When a thread exits, it does not release its resources unless it is a detached thread. The pthread_detach function sets a thread’s internal options to specify that storage for the thread can be reclaimed when the thread exits. Detached threads do not report their status when they exit. Threads that are not detached are joinable and do not release all their resources until another thread calls pthread_join for them or the entire process exits. The pthread_join function causes the caller to wait for the specified thread to exit, similar to waitpid at the process level. To prevent memory leaks, long-running programs should eventually call either pthread_detach or pthread_join for every thread.

The pthread_detach function has a single parameter, thread, the thread ID of the thread to be detached.

SYNOPSIS

   #include <pthread.h>

   int pthread_detach(pthread_t thread);
                                                     POSIX:THR

If successful, pthread_detach returns 0. If unsuccessful, pthread_detach returns a nonzero error code. The following table lists the mandatory errors for pthread_detach.

error

cause

EINVAL

thread does not correspond to a joinable thread

ESRCH

no thread with ID thread

Example 12.5. 

The following code segment creates and then detaches a thread to execute the function processfd.

void *processfd(void *arg);

int error;
int fd
pthread_t tid;

if (error = pthread_create(&tid, NULL, processfd, &fd))
   fprintf(stderr, "Failed to create thread: %s
", strerror(error));
else if (error = pthread_detach(tid))
   fprintf(stderr, "Failed to detach thread: %s
", strerror(error));

Example 12.6. detachfun.c

When detachfun is executed as a thread, it detaches itself.

#include <pthread.h>
#include <stdio.h>

void *detachfun(void *arg) {
    int i = *((int *)(arg));
    if (!pthread_detach(pthread_self()))
        return NULL;
    fprintf(stderr, "My argument is %d
", i);
    return NULL;
}

A nondetached thread’s resources are not released until another thread calls pthread_join with the ID of the terminating thread as the first parameter. The pthread_join function suspends the calling thread until the target thread, specified by the first parameter, terminates. The value_ptr parameter provides a location for a pointer to the return status that the target thread passes to pthread_exit or return. If value_ptr is NULL, the caller does not retrieve the target thread return status.

SYNOPSIS

   #include <pthread.h>

   int pthread_join(pthread_t thread, void **value_ptr);
                                                          POSIX:THR

If successful, pthread_join returns 0. If unsuccessful, pthread_join returns a nonzero error code. The following table lists the mandatory errors for pthread_join.

error

cause

EINVAL

thread does not correspond to a joinable thread

ESRCH

no thread with ID thread

Example 12.7. 

The following code illustrates how to retrieve the value passed to pthread_exit by a terminating thread.

int error;
int *exitcodep;
pthread_t tid;

if (error = pthread_join(tid, &exitcodep))
   fprintf(stderr, "Failed to join thread: %s
", strerror(error));
else
   fprintf(stderr, "The exit code was %d
", *exitcodep);

Example 12.8. 

What happens if a thread executes the following?

pthread_join(pthread_self());

Answer:

Assuming the thread was joinable (not detached), this statement creates a deadlock. Some implementations detect a deadlock and force pthread_join to return with the error EDEADLK. However, this detection is not required by the POSIX:THR Extension.

Calling pthread_join is not the only way for the main thread to block until the other threads have completed. The main thread can use a semaphore or one of the methods discussed in Section 16.6 to wait for all threads to finish.

Exiting and cancellation

The process can terminate by calling exit directly, by executing return from main, or by having one of the other process threads call exit. In any of these cases, all threads terminate. If the main thread has no work to do after creating other threads, it should either block until all threads have completed or call pthread_exit(NULL).

A call to exit causes the entire process to terminate; a call to pthread_exit causes only the calling thread to terminate. A thread that executes return from its top level implicitly calls pthread_exit with the return value (a pointer) serving as the parameter to pthread_exit. A process will exit with a return status of 0 if its last thread calls pthread_exit.

The value_ptr value is available to a successful pthread_join. However, the value_ptr in pthread_exit must point to data that exists after the thread exits, so the thread should not use a pointer to automatic local data for value_ptr.

SYNOPSIS

   #include <pthread.h>

   void pthread_exit(void *value_ptr);
                                                         POSIX:THR

POSIX does not define any errors for pthread_exit.

Threads can force other threads to return through the cancellation mechanism. A thread calls pthread_cancel to request that another thread be canceled. The target thread’s type and cancellability state determine the result. The single parameter of pthread_cancel is the thread ID of the target thread to be canceled. The pthread_cancel function does not cause the caller to block while the cancellation completes. Rather, pthread_cancel returns after making the cancellation request.

SYNOPSIS

   #include <pthread.h>

   int pthread_cancel(pthread_t thread);
                                                        POSIX:THR

If successful, pthread_cancel returns 0. If unsuccessful, pthread_cancel returns a nonzero error code. No mandatory errors are defined for pthread_cancel.

What happens when a thread receives a cancellation request depends on its state and type. If a thread has the PTHREAD_CANCEL_ENABLE state, it receives cancellation requests. On the other hand, if the thread has the PTHREAD_CANCEL_DISABLE state, the cancellation requests are held pending. By default, threads have the PTHREAD_CANCEL_ENABLE state.

The pthread_setcancelstate function changes the cancellability state of the calling thread. The pthread_setcancelstate takes two parameters: state, specifying the new state to set; and oldstate, a pointer to an integer for holding the previous state.

SYNOPSIS

   #include <pthread.h>

   int pthread_setcancelstate(int state, int *oldstate);
                                                            POSIX:THR

If successful, pthread_setcancelstate returns 0. If unsuccessful, it returns a nonzero error code. No mandatory errors are defined for pthread_setcancelstate.

Program 12.3 shows a modification of the processfd function of Program 12.1 that explicitly disables cancellation before it calls docommand, to ensure that the command won’t be canceled midstream. The original processfd always returns NULL. The processfdcancel function returns a pointer other than NULL if it cannot change the cancellation state. This function should not return a pointer to an automatic local variable, since local variables are deallocated when the function returns or the thread exits. Program 12.3 uses a parameter passed by the calling thread to return the pointer.

Example 12.3. processfdcancel.c

This function monitors a file descriptor for input and calls docommand to process the result. It explicitly disables cancellation before calling docommand.

#include <pthread.h>
#include "restart.h"
#define BUFSIZE 1024

void docommand(char *cmd, int cmdsize);

void *processfdcancel(void *arg) { /* process commands with cancellation */
   char buf[BUFSIZE];
   int fd;
   ssize_t nbytes;
   int newstate, oldstate;

   fd = *((int *)(arg));
   for ( ; ; )  {
      if ((nbytes = r_read(fd, buf, BUFSIZE)) <= 0)
         break;
      if (pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate))
         return arg;
      docommand(buf, nbytes);
      if (pthread_setcancelstate(oldstate, &newstate))
         return arg;
   }
   return NULL;
}

As a general rule, a function that changes its cancellation state or its type should restore the value before returning. A caller cannot make reliable assumptions about the program behavior unless this rule is observed. The processfdcancel function saves the old state and restores it rather than just enabling cancellation after calling docommand.

Cancellation can cause difficulties if a thread holds resources such as a lock or an open file descriptor that must be released before exiting. A thread maintains a stack of cleanup routines using pthread_cleanup_push and pthread_cleanup_pop. (We do not discuss these here.) Although a canceled thread can execute a cleanup function before exiting (not discussed here), it is not always feasible to release resources in an exit handler. Also, there may be points in the execution at which an exit would leave the program in an unacceptable state. The cancellation type allows a thread to control the point when it exits in response to a cancellation request. When its cancellation type is PTHREAD_CANCEL_ASYNCHRONOUS, the thread can act on the cancellation request at any time. In contrast, a cancellation type of PTHREAD_CANCEL_DEFERRED causes the thread to act on cancellation requests only at specified cancellation points. By default, threads have the PTHREAD_CANCEL_DEFERRED type.

The pthread_setcanceltype function changes the cancellability type of a thread as specified by its type parameter. The oldtype parameter is a pointer to a location for saving the previous type. A thread can set a cancellation point at a particular place in the code by calling pthread_testcancel. Certain blocking functions, such as read, are automatically treated as cancellation points. A thread with the PTHREAD_CANCEL_DEFERRED type accepts pending cancellation requests when it reaches such a cancellation point.

SYNOPSIS

   #include <pthread.h>

   int pthread_setcanceltype(int type, int *oldtype);
   void pthread_testcancel(void);
                                                       POSIX:THR

If successful, pthread_setcanceltype returns 0. If unsuccessful, it returns a nonzero error code. No mandatory errors are defined for pthread_setcanceltype. The pthread_testcancel has no return value.

Passing parameters to threads and returning values

The creator of a thread may pass a single parameter to a thread at creation time, using a pointer to void. To communicate multiple values, the creator must use a pointer to an array or a structure. Program 12.4 illustrates how to pass a pointer to an array. The main program passes an array containing two open file descriptors to a thread that runs copyfilemalloc.

Example 12.4. callcopymalloc.c

This program creates a thread to copy a file.

#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#define PERMS (S_IRUSR | S_IWUSR)
#define READ_FLAGS O_RDONLY
#define WRITE_FLAGS (O_WRONLY | O_CREAT | O_TRUNC)

void *copyfilemalloc(void *arg);

int main (int argc, char *argv[]) {        /* copy fromfile to tofile */
   int *bytesptr;
   int error;
   int fds[2];
   pthread_t tid;

   if (argc != 3) {
      fprintf(stderr, "Usage: %s fromfile tofile
", argv[0]);
      return 1;
   }
   if (((fds[0] = open(argv[1], READ_FLAGS)) == -1) ||
       ((fds[1] = open(argv[2], WRITE_FLAGS, PERMS)) == -1)) {
      perror("Failed to open the files");
      return 1;
   }
   if (error = pthread_create(&tid, NULL, copyfilemalloc, fds)) {
      fprintf(stderr, "Failed to create thread: %s
", strerror(error));
      return 1;
   }
   if (error = pthread_join(tid, (void **)&bytesptr)) {
      fprintf(stderr, "Failed to join thread: %s
", strerror(error));
      return 1;
   }
   printf("Number of bytes copied: %d
", *bytesptr);
   return 0;
}

Program 12.5 shows an implementation of copyfilemalloc, a function that reads from one file and outputs to another file. The arg parameter holds a pointer to a pair of open descriptors representing the source and destination files. The variables bytesp, infd and outfd are allocated on copyfilemalloc’s local stack and are not directly accessible to other threads.

Program 12.5 also illustrates a strategy for returning values from the thread. The thread allocates memory space for returning the total number of bytes copied since it is not allowed to return a pointer to its local variables. POSIX requires that malloc be thread-safe. The copyfilemalloc function returns the bytesp pointer, which is equivalent to calling pthread_exit. It is the responsibility of the calling program (callcopymalloc) to free this space when it has finished using it. In this case, the program terminates, so it is not necessary to call free.

Example 12.5. copyfilemalloc.c

The copyfilemalloc function copies the contents of one file to another by calling the copyfile function of Program 4.6 on page 100. It returns the number of bytes copied by dynamically allocating space for the return value.

#include <stdlib.h>
#include <unistd.h>
#include "restart.h"

void *copyfilemalloc(void *arg)  { /* copy infd to outfd with return value */
   int *bytesp;
   int infd;
   int outfd;

   infd = *((int *)(arg));
   outfd = *((int *)(arg) + 1);
   if ((bytesp = (int *)malloc(sizeof(int))) == NULL)
      return NULL;
   *bytesp = copyfile(infd, outfd);
   r_close(infd);
   r_close(outfd);
   return bytesp;
}

Example 12.9. 

What happens if copyfilemalloc stores the byte count in a variable with static storage class and returns a pointer to this static variable instead of dynamically allocating space for it?

Answer:

The program still works since only one thread is created. However, in a program with two copyfilemalloc threads, both store the byte count in the same place and one overwrites the other’s value.

When a thread allocates space for a return value, some other thread is responsible for freeing that space. Whenever possible, a thread should clean up its own mess rather than requiring another thread to do it. It is also inefficient to dynamically allocate space to hold a single integer. An alternative to having the thread allocate space for the return value is for the creating thread to do it and pass a pointer to this space in the argument parameter of the thread. This approach avoids dynamic allocation completely if the space is on the stack of the creating thread.

Program 12.6 creates a copyfilepass thread to copy a file. The parameter to the thread is now an array of size 3. The first two entries of the array hold the file descriptors as in Program 12.4. The third array element stores the number of bytes copied. Program 12.6 can retrieve this value either through the array or through the second parameter of pthread_join. Alternatively, callcopypass could pass an array of size 2, and the thread could store the return value over one of the incoming file descriptors.

Example 12.6. callcopypass.c

A program that creates a thread to copy a file. The parameter of the thread is an array of three integers used for two file descriptors and the number of bytes copied.

#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#define PERMS (S_IRUSR | S_IWUSR)
#define READ_FLAGS O_RDONLY
#define WRITE_FLAGS (O_WRONLY | O_CREAT | O_TRUNC)
void *copyfilepass(void *arg);

int main (int argc, char *argv[]) {
   int *bytesptr;
   int error;
   int targs[3];
   pthread_t tid;

   if (argc != 3) {
      fprintf(stderr, "Usage: %s fromfile tofile
", argv[0]);
      return 1;
   }

   if (((targs[0] = open(argv[1], READ_FLAGS)) == -1) ||
       ((targs[1] = open(argv[2], WRITE_FLAGS, PERMS)) == -1)) {
      perror("Failed to  open the files");
      return 1;
   }
   if (error = pthread_create(&tid, NULL, copyfilepass, targs)) {
      fprintf(stderr, "Failed to create thread: %s
", strerror(error));
      return 1;
   }
   if (error = pthread_join(tid, (void **)&bytesptr)) {
      fprintf(stderr, "Failed to join thread: %s
", strerror(error));
      return 1;
   }
   printf("Number of bytes copied: %d
", *bytesptr);
   return 0;
}

The copyfilepass function of Program 12.7 uses an alternative way of accessing the pieces of the argument. Compare this with the method used by the copyfilemalloc function of Program 12.5.

Example 12.7. copyfilepass.c

A thread that can be used by callcopypass to copy a file.

#include <unistd.h>
#include "restart.h"

void *copyfilepass(void *arg)  {
   int *argint;

   argint = (int *)arg;
   argint[2] = copyfile(argint[0], argint[1]);
   r_close(argint[0]);
   r_close(argint[1]);
   return argint + 2;
}

Example 12.10. 

Why have copyfilepass return a pointer to the number of bytes copied when callcopypass can access this value as args[2]?

Answer:

If a thread other than the creating thread joins with copyfilepass, it has access to the number of bytes copied through the parameter to pthread_join.

Program 12.8 shows a parallel file-copy program that uses the thread in Program 12.7. The main program has three command-line arguments: an input file basename, an output file basename and the number of files to copy. The program creates numcopiers threads. Thread i copies infile.i to outfile.i.

Example 12.11. 

What happens in Program 12.8 if a write call in copyfile of copyfilepass fails?

Answer:

The copyfilepass returns the number of bytes successfully copied, and the main program does not detect an error. You can address the issue by having copyfilepass return an error value and pass the number of bytes written in one of the elements of the array used as a parameter for thread creation.

When creating multiple threads, do not reuse the variable holding a thread’s parameter until you are sure that the thread has finished accessing the parameter. Because the variable is passed by reference, it is a good practice to use a separate variable for each thread.

Example 12.8. copymultiple.c

A program that creates threads to copy multiple file descriptors.

#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#define MAXNAME 80
#define R_FLAGS O_RDONLY
#define W_FLAGS (O_WRONLY | O_CREAT)
#define W_PERMS (S_IRUSR | S_IWUSR)

typedef struct {
   int args[3];
   pthread_t tid;
} copy_t;

void *copyfilepass(void *arg);

int main(int argc, char *argv[]) {
   int *bytesp;
   copy_t *copies;
   int error;
   char filename[MAXNAME];
   int i;
   int numcopiers;
   int totalbytes = 0;

   if (argc != 4) {
      fprintf(stderr, "Usage: %s infile outfile copies
", argv[0]);
      return 1;
   }
   numcopiers = atoi(argv[3]);
   if ((copies = (copy_t *)calloc(numcopiers, sizeof(copy_t))) == NULL) {
      perror("Failed to allocate copier space");
      return 1;
   }
              /* open the source and destination files and create the threads */
   for (i = 0; i < numcopiers; i++) {
      copies[i].tid = pthread_self();       /* cannot be value for new thread */
      if (snprintf(filename, MAXNAME, "%s.%d", argv[1], i+1) == MAXNAME) {
         fprintf(stderr, "Input filename %s.%d too long", argv[1], i + 1);
         continue;
      }
      if ((copies[i].args[0] = open(filename, R_FLAGS)) == -1) {
         fprintf(stderr, "Failed to open source file %s: %s
",
                         filename, strerror(errno));
         continue;
      }
     if (snprintf(filename, MAXNAME, "%s.%d", argv[2], i+1) == MAXNAME) {
         fprintf(stderr, "Output filename %s.%d too long", argv[2], i + 1);
         continue;
      }
      if ((copies[i].args[1] = open(filename, W_FLAGS, W_PERMS)) == -1) {
         fprintf(stderr, "Failed to open destination file %s: %s
",
                         filename, strerror(errno));
         continue;
      }
      if (error = pthread_create((&copies[i].tid), NULL,
                                  copyfilepass, copies[i].args)) {
         fprintf(stderr, "Failed to create thread %d: %s
", i + 1,
                 strerror(error));
         copies[i].tid = pthread_self();    /* cannot be value for new thread */
      }

   }
                     /* wait for the threads to finish and report total bytes */
   for (i = 0; i < numcopiers; i++) {
      if (pthread_equal(copies[i].tid, pthread_self()))        /* not created */
         continue;
      if (error = pthread_join(copies[i].tid, (void**)&bytesp)) {
         fprintf(stderr, "Failed to join thread %d
", i);
         continue;
      }
      if (bytesp == NULL) {
         fprintf(stderr, "Thread %d failed to return status
", i);
         continue;
      }
      printf("Thread %d copied %d bytes from %s.%d to %s.%d
",
             i, *bytesp, argv[1], i + 1, argv[2], i + 1);
      totalbytes += *bytesp;
   }
   printf("Total bytes copied = %d
", totalbytes);
   return 0;
}

Program 12.9 shows a simple example of what can go wrong. The program creates 10 threads that each output the value of their parameter. The main program uses the thread creation loop index i as the parameter it passes to the threads. Each thread prints the value of the parameter it received. A thread can get an incorrect value if the main program changes i before the thread has a chance to print it.

Example 12.12. 

Run Program 12.9 and examine the results. What parameter value is reported by each thread?

Answer:

The results vary, depending on how the system schedules threads. One possibility is that main completes the loop creating the threads before any thread prints the value of the parameter. In this case, all the threads print the value 10.

Example 12.9. badparameters.c

A program that incorrectly passes parameters to multiple threads.

#include <pthread.h>
#include <stdio.h>
#include <string.h>
#define NUMTHREADS 10

static void *printarg(void *arg) {
   fprintf(stderr, "Thread received %d
", *(int *)arg);
   return NULL;
}

int main (void) {        /* program incorrectly passes parameters to threads */
   int error;
   int i;
   int j;
   pthread_t tid[NUMTHREADS];

   for (i = 0; i < NUMTHREADS; i++)
      if (error = pthread_create(tid + i, NULL, printarg, (void *)&i)) {
         fprintf(stderr, "Failed to create thread: %s
", strerror(error));
         tid[i] = pthread_self();
      }
   for (j = 0; j < NUMTHREADS; j++)
      if (pthread_equal(pthread_self(), tid[j]))
         continue;
      if (error = pthread_join(tid[j], NULL))
         fprintf(stderr, "Failed to join thread: %s
", strerror(error));
   printf("All threads done
");
   return 0;
}

Example 12.13. 

For each of the following, start with Program 12.9 and make the specified modifications. Predict the output, and then run the program to see if you are correct.

1

Run the original program without any modification.

2

Put a call to sleep(1); at the start of printarg.

3

Put a call to sleep(1); inside the first for loop after the call to pthread_create.

4

Put a call to sleep(1); after the first for loop.

5.-8.

Repeat each of the items above, using i as the loop index rather than j.

Answer:

The results may vary if it takes more than a second for the threads to execute. On a fast enough system, the result will be something like the following.

  1. Output described in Exercise 12.12.

  2. Each thread outputs the value 10, the value of i when main has finished its loop.

  3. Each thread outputs the correct value since it executes before the value of i changes.

  4. Same as in Exercise 12.12.

  5. All threads output the value 0, the value of i when main waits for the first thread to terminate. The results may vary.

  6. Same as 5.

  7. Same as 3.

  8. Same as 4.

Example 12.14. whichexit.c

The whichexit function can be executed as a thread.

#include <errno.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>

void *whichexit(void *arg) {
   int n;
   int np1[1];
   int *np2;
   char s1[10];
   char s2[] = "I am done";
   n = 3;
   np1[0] = n;
   np2 = (int *)malloc(sizeof(int *));
   *np2 = n;
   strcpy(s1, "Done");
   return(NULL);
}

Which of the following would be safe replacements for NULL as the parameter to pthread_exit? Assume no errors occur.

  1. n

  2. &n

  3. (int *)n

  4. np1

  5. np2

  6. s1

  7. s2

  8. "This works"

  9. strerror(EINTR)

Answer:

  1. The return value is a pointer, not an integer, so this is invalid.

  2. The integer n has automatic storage class, so it is illegal to access it after the function terminates.

  3. This is a common way to return an integer from a thread. The integer is cast to a pointer. When another thread calls pthread_join for this thread, it casts the pointer back to an integer. While this will probably work in most implementations, it should be avoided. The C standard [56, Section 6.3.2.3] says that an integer may be converted to a pointer or a pointer to an integer, but the result is implementation defined. It does not guarantee that the result of converting an integer to a pointer and back again yields the original integer.

  4. The array np1 has automatic storage class, so it is illegal to access the array after the function terminates.

  5. This is safe since the dynamically allocated space will be available until it is freed.

  6. The array s1 has automatic storage class, so it is illegal to access the array after the function terminates.

  7. The array s2 has automatic storage class, so it is illegal to access the array after the function terminates.

  8. This is valid in C, since string literals have static storage duration.

  9. This is certainly invalid if strerror is not thread-safe. Even on a system where strerror is thread-safe, the string produced is not guaranteed to be available after the thread terminates.

Thread Safety

A hidden problem with threads is that they may call library functions that are not thread-safe, possibly producing spurious results. A function is thread-safe if multiple threads can execute simultaneous active invocations of the function without interference. POSIX specifies that all the required functions, including the functions from the standard C library, be implemented in a thread-safe manner except for the specific functions listed in Table 12.2. Those functions whose traditional interfaces preclude making them thread-safe must have an alternative thread-safe version designated with an _r suffix.

An important example of a function that does not have to be thread-safe is strerror. Although strerror is not guaranteed to be thread-safe, many systems have implemented this function in a thread-safe manner. Unfortunately, because strerror is listed in Table 12.2, you can not assume that it works correctly if multiple threads call it. We use strerror only in the main thread, often to produce error messages for pthread_create and pthread_join. Section 13.7 gives a thread-safe implementation called strerror_r.

Another interaction problem occurs when threads access the same data. The individual copier threads in Program 12.8 work on independent problems and do not interact with each other. In more complicated applications, a thread may not exit after completing its assigned task. Instead, a worker thread may request additional tasks or share information. Chapter 13 explains how to control this type of interaction by using synchronization primitives such as mutex locks and condition variables.

Table 12.2. POSIX functions that are not required to be thread-safe.

asctime

fcvt

getpwnam

nl_langinfo

basename

ftw

getpwuid

ptsname

catgets

gcvt

getservbyname

putc_unlocked

crypt

getc_unlocked

getservbyport

putchar_unlocked

ctime

getchar_unlocked

getservent

putenv

dbm_clearerr

getdate

getutxent

pututxline

dbm_close

getenv

getutxid

rand

dbm_delete

getgrent

getutxline

readdir

dbm_error

getgrgid

gmtime

setenv

dbm_fetch

getgrnam

hcreate

setgrent

dbm_firstkey

gethostbyaddr

hdestroy

setkey

dbm_nextkey

gethostbyname

hsearch

setpwent

dbm_open

gethostent

inet_ntoa

setutxent

dbm_store

getlogin

l64a

strerror

dirname

getnetbyaddr

lgamma

strtok

dlerror

getnetbyname

lgammaf

ttyname

drand48

getnetent

lgammal

unsetenv

ecvt

getopt

localeconv

wcstombs

encrypt

getprotobyname

localtime

wctomb

endgrent

getprotobynumber

lrand48

 

endpwent

getprotoent

mrand48

 

endutxent

getpwent

nftw

 

In traditional UNIX implementations, errno is a global external variable that is set when system functions produce an error. This implementation does not work for multithreading (see Section 2.7), and in most thread implementations errno is a macro that returns thread-specific information. In essence, each thread has a private copy of errno. The main thread does not have direct access to errno for a joined thread, so if needed, this information must be returned through the last parameter of pthread_join.

User Threads versus Kernel Threads

The two traditional models of thread control are user-level threads and kernel-level threads. User-level threads, shown in Figure 12.3, usually run on top of an existing operating system. These threads are invisible to the kernel and compete among themselves for the resources allocated to their encapsulating process. The threads are scheduled by a thread runtime system that is part of the process code. Programs with user-level threads usually link to a special library in which each library function is enclosed by a jacket. The jacket function calls the thread runtime system to do thread management before and possibly after calling the jacketed library function.

User-level threads are not visible outside their encapsulating process.

Figure 12.3. User-level threads are not visible outside their encapsulating process.

Functions such as read or sleep can present a problem for user-level threads because they may cause the process to block. To avoid blocking the entire process on a blocking call, the user-level thread library replaces each potentially blocking call in the jacket by a nonblocking version. The thread runtime system tests to see if the call would cause the thread to block. If the call would not block, the runtime system does the call right away. If the call would block, however, the runtime system places the thread on a list of waiting threads, adds the call to a list of actions to try later, and picks another thread to run. All this control is invisible to the user and to the operating system.

User-level threads have low overhead, but they also have some disadvantages. The user thread model, which assumes that the thread runtime system will eventually regain control, can be thwarted by CPU-bound threads. A CPU-bound thread rarely performs library calls and may prevent the thread runtime system from regaining control to schedule other threads. The programmer has to avoid the lockout situation by explicitly forcing CPU-bound threads to yield control at appropriate points. A second problem is that user-level threads can share only processor resources allocated to their encapsulating process. This restriction limits the amount of available parallelism because the threads can run on only one processor at a time. Since one of the prime motivations for using threads is to take advantage of multiprocessor workstations, user-level threads alone are not an acceptable approach.

With kernel-level threads, the kernel is aware of each thread as a schedulable entity and threads compete systemwide for processor resources. Figure 12.4 illustrates the visibility of kernel-level threads. The scheduling of kernel-level threads can be almost as expensive as the scheduling of processes themselves, but kernel-level threads can take advantage of multiple processors. The synchronization and sharing of data for kernel-level threads is less expensive than for full processes, but kernel-level threads are considerably more expensive to manage than user-level threads.

Operating system schedules kernel-level threads as though they were individual processes.

Figure 12.4. Operating system schedules kernel-level threads as though they were individual processes.

Hybrid thread models have advantages of both user-level and kernel-level models by providing two levels of control. Figure 12.5 illustrates a typical hybrid approach. The user writes the program in terms of user-level threads and then specifies how many kernel-schedulable entities are associated with the process. The user-level threads are mapped into the kernel-schedulable entities at runtime to achieve parallelism. The level of control that a user has over the mapping depends on the implementation. In the Sun Solaris thread implementation, for example, the user-level threads are called threads and the kernel-schedulable entities are called lightweight processes. The user can specify that a particular thread be run by a dedicated lightweight process or that a particular group of threads be run by a pool of lightweight processes.

Hybrid model has two levels of scheduling, with user-level threads mapped into kernel entities.

Figure 12.5. Hybrid model has two levels of scheduling, with user-level threads mapped into kernel entities.

The POSIX thread scheduling model is a hybrid model that is flexible enough to support both user-level and kernel-level threads in particular implementations of the standard. The model consists of two levels of scheduling—threads and kernel entities. The threads are analogous to user-level threads. The kernel entities are scheduled by the kernel. The thread library decides how many kernel entities it needs and how they will be mapped.

POSIX introduces the idea of a thread-scheduling contention scope, which gives the programmer some control over how kernel entities are mapped to threads. A thread can have a contentionscope attribute of either PTHREAD_SCOPE_PROCESS or PTHREAD_SCOPE_SYSTEM. Threads with the PTHREAD_SCOPE_PROCESS attribute contend for processor resources with the other threads in their process. POSIX does not specify how such a thread contends with threads outside its own process, so PTHREAD_SCOPE_PROCESS threads can be strictly user-level threads or they can be mapped to a pool of kernel entities in some more complicated way.

Threads with the PTHREAD_SCOPE_SYSTEM attribute contend systemwide for processor resources, much like kernel-level threads. POSIX leaves the mapping between PTHREAD_SCOPE_SYSTEM threads and kernel entities up to the implementation, but the obvious mapping is to bind such a thread directly to a kernel entity. A POSIX thread implementation can support PTHREAD_SCOPE_PROCESS, PTHREAD_SCOPE_SYSTEM or both. You can get the scope with pthread_attr_getscope and set the scope with pthread_attr_setscope, provided that your POSIX implementation supports both the POSIX:THR Thread Extension and the POSIX:TPS Thread Execution Scheduling Extension.

Thread Attributes

POSIX takes an object-oriented approach to representation and assignment of properties by encapsulating properties such as stack size and scheduling policy into an object of type pthread_attr_t. The attribute object affects a thread only at the time of creation. You first create an attribute object and associate properties, such as stack size and scheduling policy, with the attribute object. You can then create multiple threads with the same properties by passing the same thread attribute object to pthread_create. By grouping the properties into a single object, POSIX avoids pthread_create calls with a large number of parameters.

Table 12.3 shows the settable properties of thread attributes and their associated functions. Other entities, such as condition variables and mutex locks, have their own attribute object types. Chapter 13 discusses these synchronization mechanisms.

Table 12.3. Summary of settable properties for POSIX thread attribute objects.

property

function

attribute objects

pthread_attr_destroy

pthread_attr_init

state

pthread_attr_getdetachstate

pthread_attr_setdetachstate

stack

pthread_attr_getguardsize

pthread_attr_setguardsize

pthread_attr_getstack

pthread_attr_setstack

scheduling

pthread_attr_getinheritsched

pthread_attr_setinheritsched

pthread_attr_getschedparam

pthread_attr_setschedparam

pthread_attr_getschedpolicy

pthread_attr_setschedpolicy

pthread_attr_getscope

pthread_attr_setscope

The pthread_attr_init function initializes a thread attribute object with the default values. The pthread_attr_destroy function sets the value of the attribute object to be invalid. POSIX does not specify the behavior of the object after it has been destroyed, but the variable can be initialized to a new thread attribute object. Both pthread_attr_init and pthread_attr_destroy take a single parameter that is a pointer to a pthread_attr_t attribute object.

SYNOPSIS

   #include <pthread.h>

   int pthread_attr_destroy(pthread_attr_t *attr);
   int pthread_attr_init(pthread_attr_t *attr);
                                                            POSIX:THR

If successful, pthread_attr_destroy and pthread_attr_init return 0. If unsuccessful, these functions return a nonzero error code. The pthread_attr_init function sets errno to ENOMEM if there is not enough memory to create the thread attribute object.

Most of the get/set thread attribute functions have two parameters. The first parameter is a pointer to a thread attribute object. The second parameter is the new value of the attribute for a set operation or a pointer to location to hold the value for a get operation. The pthread_attr_getstack and pthread_attr_setstack each have one additional parameter.

The thread state

The pthread_attr_getdetachstate function examines the state of an attribute object, and the pthread_attr_setdetachstate function sets the state of an attribute object. The possible values of the thread state are PTHREAD_CREATE_JOINABLE and PTHREAD_CREATE_DETACHED. The attr parameter is a pointer to the attribute object. The detachstate parameter corresponds to the value to be set for pthread_attr_setdetachstate and to a pointer to the value to be retrieved for pthread_attr_getdetachstate.

SYNOPSIS

  #include <pthread.h>

  int pthread_attr_getdetachstate(const pthread_attr_t *attr,
                                  int *detachstate);
  int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
                                                                   POSIX:THR

If successful, these functions return 0. If unsuccessful, they return a nonzero error code. The pthread_attr_setdetachstate function sets errno to EINVAL if detachstate is invalid.

Detached threads release their resources when they terminate, whereas joinable threads should be waited for with a pthread_join. A thread that is detached cannot be waited for with a pthread_join. By default, threads are joinable. You can detach a thread by calling the pthread_detach function after creating the thread. Alternatively, you can create a thread in the detached state by using an attribute object with thread state PTHREAD_CREATE_DETACHED.

Example 12.15. 

The following code segment creates a detached thread to run processfd.

int error, fd;
pthread_attr_t tattr;
pthread_t tid;

if (error = pthread_attr_init(&tattr))
   fprintf(stderr, "Failed to create attribute object: %s
",
                    strerror(error));
else if (error = pthread_attr_setdetachstate(&tattr,
                 PTHREAD_CREATE_DETACHED))
   fprintf(stderr, "Failed to set attribute state to detached: %s
",
           strerror(error));
else if (error = pthread_create(&tid, &tattr, processfd, &fd))
   fprintf(stderr, "Failed to create thread: %s
", strerror(error));

The thread stack

A thread has a stack whose location and size are user-settable, a useful property if the thread stack must be placed in a particular region of memory. To define the placement and size of the stack for a thread, you must first create an attribute object with the specified stack attributes. Then, call pthread_create with this attribute object.

The pthread_attr_getstack function examines the stack parameters, and the pthread_attr_setstack function sets the stack parameters of an attribute object. The attr parameter of each function is a pointer to the attribute object. The pthread_attr_setstack function takes the stack address and stack size as additional parameters. The pthread_attr_getstack takes pointers to these items.

SYNOPSIS

  #include <pthread.h>

  int pthread_attr_getstack(const pthread_attr_t *restrict attr,
           void **restrict stackaddr, size_t *restrict stacksize);
  int pthread_attr_setstack(pthread_attr_t *attr,
           void *stackaddr, size_t stacksize);
                                                            POSIX:THR,TSA,TSS

If successful, the pthread_attr_getstack and pthread_attr_setstack functions return 0. If unsuccessful, these functions return a nonzero error code. The pthread_attr_setstack function sets errno to EINVAL if stacksize is out of range.

POSIX also provides functions for examining or setting a guard for stack overflows if the stackaddr has not been set by the user. The pthread_attr_getguardsize function examines the guard parameters, and the pthread_attr_setguardsize function sets the guard parameters for controlling stack overflows in an attribute object. If the guardsize parameter is 0, the stack is unguarded. For a nonzero guardsize, the implementation allocates additional memory of at least guardsize. An overflow into this extra memory causes an error and may generate a SIGSEGV signal for the thread.

SYNOPSIS

  #include <pthread.h>

  int pthread_attr_getguardsize(const pthread_attr_t *restrict attr,
                            size_t *restrict guardsize);
  int pthread_attr_setguardsize(pthread_attr_t *attr,
                            size_t guardsize);
                                                                 POSIX:THR,XSI

If successful, pthread_attr_getguardsize and pthread_attr_setguardsize return 0. If unsuccessful, these functions return a nonzero error code. They return EINVAL if the attr or guardsize parameter is invalid. Guards require the POSIX:THR Extension and the POSIX:XSI Extension.

Thread scheduling

The contention scope of an object controls whether the thread competes within the process or at the system level for scheduling resources. The pthread_attr_getscope examines the contention scope, and the pthread_attr_setscope sets the contention scope of an attribute object. The attr parameter is a pointer to the attribute object. The contentionscope parameter corresponds to the value to be set for pthread_attr_setscope and to a pointer to the value to be retrieved for pthread_attr_getscope. The possible values of the contentionscope parameter are PTHREAD_SCOPE_PROCESS and PTHREAD_SCOPE_SYSTEM.

SYNOPSIS

  #include <pthread.h>

  int pthread_attr_getscope(const pthread_attr_t *restrict attr,
                            int *restrict contentionscope);
  int pthread_attr_setscope(pthread_attr_t *attr, int contentionscope);
                                                                POSIX:THR,TPS

If successful, pthread_attr_getscope and pthread_attr_setscope return 0. If unsuccessful, these functions return a nonzero error code. No mandatory errors are defined for these functions.

Example 12.16. 

The following code segment creates a thread that contends for kernel resources.

int error;
int fd;
pthread_attr_t tattr;
pthread_t tid;

if (error = pthread_attr_init(&tattr))
   fprintf(stderr, "Failed to create an attribute object:%s
",
           strerror(error));
else if (error = pthread_attr_setscope(&tattr, PTHREAD_SCOPE_SYSTEM))
   fprintf(stderr, "Failed to set scope to system:%s
",
           strerror(error));
else if (error = pthread_create(&tid, &tattr, processfd, &fd))
   fprintf(stderr, "Failed to create a thread:%s
", strerror(error));

POSIX allows a thread to inherit a scheduling policy in different ways. The pthread_attr_getinheritsched function examines the scheduling inheritance policy, and the pthread_attr_setinheritsched function sets the scheduling inheritance policy of an attribute object.

The attr parameter is a pointer to the attribute object. The inheritsched parameter corresponds to the value to be set for pthread_attr_setinheritsched and to a pointer to the value to be retrieved for pthread_attr_getinheritsched. The two possible values of inheritsched are PTHREAD_INHERIT_SCHED and PTHREAD_EXPLICIT_SCHED. The value of inheritsched determines how the other scheduling attributes of a created thread are to be set. With PTHREAD_INHERIT_SCHED, the scheduling attributes are inherited from the creating thread and the other scheduling attributes are ignored. With PTHREAD_EXPLICIT_SCHED, the scheduling attributes of this attribute object are used.

SYNOPSIS

  #include <pthread.h>

  int pthread_attr_getinheritsched(const pthread_attr_t *restrict attr,
                            int *restrict inheritsched);
  int pthread_attr_setinheritsched(pthread_attr_t *attr,
                            int inheritsched);
                                                                POSIX:THR,TPS

If successful, these functions return 0. If unsuccessful, they return a nonzero error code. No mandatory errors are defined for these functions.

The pthread_attr_getschedparam function examines the scheduling parameters, and the pthread_attr_setschedparam sets the scheduling parameters of an attribute object. The attr parameter is a pointer to the attribute object. The param parameter is a pointer to the value to be set for pthread_attr_setschedparam and a pointer to the value to be retrieved for pthread_attr_getschedparam. Notice that unlike the other pthread_attr_set functions, the second parameter is a pointer because it corresponds to a structure rather than an integer. Passing a structure by value is inefficient.

SYNOPSIS

  #include <pthread.h>

  int pthread_attr_getschedparam(const pthread_attr_t *restrict attr,
                            struct sched_param *restrict param);
  int pthread_attr_setschedparam(pthread_attr_t *restrict attr,
                            const struct sched_param *restrict param);
                                                                   POSIX:THR

If successful, these functions return 0. If unsuccessful, they return a nonzero error code. No mandatory errors are defined for these functions.

The scheduling parameters depend on the scheduling policy. They are encapsulated in a struct sched_param structure defined in sched.h. The SCHED_FIFO and SCHED_RR scheduling policies require only the sched_priority member of struct sched_param. The sched_priority field holds an int priority value, with larger priority values corresponding to higher priorities. Implementations must support at least 32 priorities.

Program 12.10 shows a function that creates a thread attribute object with a specified priority. All the other attributes have their default values. Program 12.10 returns a pointer to the created attribute object or NULL if the function failed, in which case it sets errno. Program 12.10 illustrates the general strategy for changing parameters—read the existing values first and change only the ones that you need to change.

Example 12.17. 

The following code segment creates a dothis thread with the default attributes, except that the priority is HIGHPRIORITY.

#define HIGHPRIORITY 10

int fd;
pthread_attr_t *tattr;
pthread_t tid;
struct sched_param tparam;

if ((tattr = makepriority(HIGHPRIORITY))) {
   perror("Failed to create the attribute object");
else if (error = pthread_create(&tid, tattr, dothis, &fd))
   fprintf(stderr, "Failed to create dothis thread:%s
", strerror(error));

Threads of the same priority compete for processor resources as specified by their scheduling policy. The sched.h header file defines SCHED_FIFO for first-in-first-out scheduling, SCHED_RR for round-robin scheduling and SCHED_OTHER for some other policy. One additional scheduling policy, SCHED_SPORADIC, is defined for implementations supporting the POSIX:SS Process Sporadic Server Extension and the POSIX:TSP Thread Sporadic Server Extension. Implementations may also define their own policies.

Example 12.10. makepriority.c

A function to create a thread attribute object with the specified priority.

#include <errno.h>
#include <pthread.h>
#include <stdlib.h>

pthread_attr_t *makepriority(int priority) {    /* create attribute object */
   pthread_attr_t *attr;
   int error;
   struct sched_param param;

   if ((attr = (pthread_attr_t *)malloc(sizeof(pthread_attr_t))) == NULL)
      return NULL;
   if (!(error = pthread_attr_init(attr)) &&
       !(error = pthread_attr_getschedparam(attr, &param))) {
       param.sched_priority = priority;
       error = pthread_attr_setschedparam(attr, &param);
   }
   if (error) {                      /* if failure, be sure to free memory */
      free(attr);
      errno = error;
      return NULL;
   }
   return attr;
}

First-in-first-out scheduling policies (e.g., SCHED_FIFO) use a queue for threads in the runnable state at a specified priority. Blocked threads that become runnable are put at the end of the queue corresponding to their priority, whereas running threads that have been preempted are put at the front of their queue.

Round-robin scheduling (e.g., SCHED_RR) behaves similarly to first-in-first-out except that when a running thread has been running for its quantum, it is put at the end of the queue for its priority. The sched_rr_get_interval function returns the quantum.

Sporadic scheduling, which is similar to first-in-first-out, uses two parameters (the replenishment period and the execution capacity) to control the number of threads running at a given priority level. The rules are reasonably complex, but the policy allows a program to more easily regulate the number of threads competing for the processor as a function of available resources.

Preemptive priority policy is the most common implementation of SCHED_OTHER. A POSIX-compliant implementation can support any of these scheduling policies. The actual behavior of the policy in the implementation depends on the scheduling scope and other factors.

The pthread_attr_getschedpolicy function gets the scheduling policy, and the pthread_attr_setschedpolicy function sets the scheduling policy of an attribute object. The attr parameter is a pointer to the attribute object. For the function pthread_attr_setschedpolicy, the policy parameter is a pointer to the value to be set; for pthread_attr_getschedpolicy, it is a pointer to the value to be retrieved. The scheduling policy values are described above.

SYNOPSIS

  #include <pthread.h>

  int pthread_attr_getschedpolicy(const pthread_attr_t *restrict attr,
                                 int *restrict policy);
  int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
                                                                   POSIX:THR

If successful, these functions return 0. If unsuccessful, they return a nonzero error code. No mandatory errors are defined for these functions.

Exercise: Parallel File Copy

This section develops a parallel file copy as an extension of the copier application of Program 12.8. Be sure to use thread-safe calls in the implementation. The main program takes two command-line arguments that are directory names and copies everything from the first directory into the second directory. The copy program preserves subdirectory structure. The same filenames are used for source and destination. Implement the parallel file copy as follows.

  1. Write a function called copydirectory that has the following prototype.

    void *copydirectory(void *arg)
    

    The copydirectory function copies all the files from one directory to another directory. The directory names are passed in arg as two consecutive strings (separated by a null character). Assume that both source and destination directories exist when copydirectory is called. In this version, only ordinary files are copied and subdirectories are ignored. For each file to be copied, create a thread to run the copyfilepass function of Program 12.7. For this version, wait for each thread to complete before creating the next one.

  2. Write a main program that takes two command-line arguments for the source and destination directories. The main program creates a thread to run copydirectory and then does a pthread_join to wait for the copydirectory thread to complete. Use this program to test the first version of copydirectory.

  3. Modify the copydirectory function so that if the destination directory does not exist, copydirectory creates the directory. Test the new version.

  4. Modify copydirectory so that after it creates a thread to copy a file, it continues to create threads to copy the other files. Keep the thread ID and open file descriptors for each copyfilepass thread in a linked list with a node structure similar to the following.

    typedef struct copy_struct {
       char *namestring;
       int sourcefd;
       int destinationfd;
       int bytescopied;
       pthread_t tid;
       struct copy_struct *next;
    } copyinfo_t;
    copyinfo_t *head = NULL;
    copyinfo_t *tail = NULL;
    

    After the copydirectory function creates threads to copy all the files in the directory, it does a pthread_join on each thread in its list and frees the copyinfo_t structure.

  5. Modify the copyfilepass function of Program 12.7 so that its parameter is a pointer to a copyinfo_t structure. Test the new version of copyfilepass and copydirectory.

  6. Modify copydirectory so that if a file is a directory instead of an ordinary file, copydirectory creates a thread to run copydirectory instead of copyfilepass. Test the new function.

  7. Devise a method for performing timings to compare an ordinary copy with the threaded copy.

  8. If run on a large directory, the program may attempt to open more file descriptors or more threads than are allowed for a process. Devise a method for handling this situation.

  9. See whether there is a difference in running time if the threads have scope PTHREAD_SCOPE_SYSTEM instead of PTHREAD_SCOPE_PROCESS.

Additional Reading

A number of books on POSIX thread programming are available. They include Programming with POSIX(R) Threads by Butenhof [19], Pthreads Programming: A POSIX Standard for Better Multiprocessing by Nichols et al. [87], Multithreaded Programming with Pthreads by Lewis and Berg [72] and Thread Time: The Multithreaded Programming Guide by Norton and DiPasquale. All these books are based on the original POSIX standard. The book Distributed Operating Systems by Tanenbaum [121] presents an understandable general discussion of threads. Approaches to thread scheduling are discussed in [2, 12, 32, 78]. Finally, the POSIX standard [49, 51] is a surprisingly readable account of the conflicting issues and choices involved in implementing a usable threads package.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset