-2

In Computer Systems: a Programmer's Perspective,

12.7.1 Thread Safety

When we program with threads, we must be careful to write functions that have a property called thread safety. A function is said to be thread-safe if and only if it will always produce correct results when called repeatedly from multiple concurrent threads. If a function is not thread-safe, then we say it is thread-unsafe. We can identify four (nondisjoint) classes of thread-unsafe functions:

Class 1: Functions that do not protect shared variables. We have already encountered this problem with the thread function in Figure 12.16, which increments an unprotected global counter variable cnt. This class of thread-unsafe functions is relatively easy to make thread-safe: protect the shared variables with synchronization operations such as P and V . An advantage is that it does not require any changes in the calling program. A disadvantage is that the synchronization operations slow down the function.

/* WARNING: This code is buggy! */
#include "csapp.h"

void *thread(void *vargp); /* Thread routine prototype */

/* Global shared variable */
volatile long cnt = 0; /* Counter */

int main(int argc, char **argv)
{
  long niters;
  pthread_t tid1, tid2;

  /* Check input argument */
  if (argc != 2) {
    printf("usage: %s <niters>\n", argv[0]);
    exit(0);
  }
  niters = atoi(argv[1]);

  /* Create threads and wait for them to finish */
  Pthread_create(&tid1, NULL, thread, &niters);
  Pthread_create(&tid2, NULL, thread, &niters);
  Pthread_join(tid1, NULL);
  Pthread_join(tid2, NULL);

  /* Check result */
  if (cnt != (2 * niters))
    printf("BOOM! cnt=%ld\n", cnt);
  else
    printf("OK cnt=%ld\n", cnt);
  exit(0);
}

/* Thread routine */
void *thread(void *vargp)
{
  long i, niters = *((long *)vargp);

  for (i = 0; i < niters; i++)
    cnt++;

  return NULL;
}

**Class 2: Functions that keep state across multiple invocations.**A pseudorandom number generator is a simple example of this class of thread-unsafe functions. Consider the pseudorandom number generator package in Figure 12.37. The rand function is thread-unsafe because the result of the current invocation depends on an intermediate result from the previous iteration. When we call rand repeatedly from a single thread after seeding it with a call to srand, we can expect a repeatable sequence of numbers. However, this assumption no longer holds if multiple threads are calling rand. The only way to make a function such as rand thread-safe is to rewrite it so that it does not use any static data, relying instead on the caller to pass the state information in arguments. The disadvantage is that the programmer is now forced to change the code in the calling routine as well. In a large program where there are potentially hundreds of different call sites, making such modifications could be nontrivial and prone to error.

unsigned next_seed = 1;

/* rand - return pseudorandom integer in the range 0..32767 */
unsigned rand(void)
{
  next_seed = next_seed*1103515245 + 12543;
  return (unsigned)(next_seed>>16) % 32768;
}

/* srand - set the initial seed for rand() */
void srand(unsigned new_seed)
{
  next_seed = new_seed;
}
  1. In the example in case 2, according to the solution in the quote, should the "rand()" function be changed to "unsigned rand(unsigned next_seed)"?

    /* rand - return pseudorandom integer in the range 0..32767 */
    unsigned rand(unsigned next_seed)
    {
      return (unsigned)(next_seed>>16) % 32768;
    }
    

    Then how should the new rand() be used to avoid problems? (I guess rand() is to be called by the function passed as the third argument to pthread_create().)

  2. What are the differences between the problem of Class 2 and the problem of Class 1? (Very similar, aren't they?)

Thanks.

0
1

In the example in case 2, according to the solution in the quote, should the "rand()" function be changed to "unsigned rand(unsigned next_seed)"?

/* rand - return pseudorandom integer in the range 0..32767 */
unsigned rand(unsigned next_seed)
{
  return (unsigned)(next_seed>>16) % 32768;
}

Then how should the new rand() be used to avoid problems? (I guess rand() is to be called by the function passed as the third argument to pthread_create().)

A thread-safe rand (and srand) could look something like this:

typedef struct {
  unsigned next_seed;
} rand_state;

/* srand - set the initial seed for rand() */
rand_state srand(unsigned new_seed)
{
  rand_state state;
  state.next_seed = new_seed;
  return state;
}

/* rand - return pseudorandom integer in the range 0..32767 */
unsigned rand(rand_state* state)
{
  state->next_seed = state->next_seed*1103515245 + 12543;
  return (unsigned)(state->next_seed>>16) % 32768;
}

The caller of this version would have to store the rand_state object that was received from srand and pass that to each call of rand.

What are the differences between the problem of Class 2 and the problem of Class 1? (Very similar, aren't they?)

They look very similar, because the problems of class 2 are a subset of the problems of class 1.

The problems of class 1 only show their nature when two threads try to access the shared state at exactly the same time. Those problems can be resolved by synchronization primitives that make it impossible for two threads to access the same data at the same time.

The rand example given for the class 2 problem is also a class 1 problem. If two different threads call rand at exactly the same time, all kinds of funny stuff can happen. That funny stuff can be prevented by using synchronization primitives in rand.

However, rand is defined to produce a sequence of pseudo random numbers and for a given seed value, that sequence should always be the same. But when two threads use rand, then neither thread will consistently see the same sequence of pseudo-random numbers, even when the calls to rand are properly separated in time. This is what the second class of thread-unsafe functions is about: Due to the sharing of state with itself, two or more threads using a particular function can cause that the function does not fulfil its contract.

Functions that are considered to be non-re-entrant are typically also class 2 non-thread-safe functions.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.