Return to the lecture notes index

Lecture 19: Tuesday, December 4, 2006

Threads

Before Thanksgiving, we took some time to review the nature of processes, tasks, and threads. We also gave some consideration to the evolution and mechanisms behind user-level and kernel-support threads

Today, we're just going to take a look at some basic examples. When linking them, or your own code, don't forget "-lpthread".

Global Variables and "Volatile"

When I was a student, first learning threaded programming, I was bitten by a unique interaction of the "-O" optimize flag and the use of global variables.

My partner and I structured our code well: The worker thread code in one file, the supervisor thread code in another file, the main() in another file, and globals in their own file. Each implementation file had its own header file.

Since we hadn't yet learned about synchronization primitives, we used a rudimentary spin lock. It worked like this:

globals.c:

  int ready = 0;
  

init function:

  // initialize various stuff
  ready = 1;
  

worker thread function

  /* Wait for intialization to happen */
  while (!ready)
  ;

  /* Do worker thread stuff (/
  

The program worked well. Then, when my partern and I were done, we removed the "-g" flag and added the "-O" flag. It broke. It hung forever. We searched for hours for memory errors. At the time, we thought, "Memory errors are the only thing that breaks an otherwise working program upon optimization." Well, um, not really.

After hours of reading and instrumenting code, I about flipped. The debugger showed the program spinning endlessly wihtin the while loop. So, I disassembled the program and looked directly at the related piece of code.

To make a long story short, the problem was this. When optimization is turned on, the compiler tooks to do things to make the code faster. Certain of these things relate to details of the machine. Certain of them relate to the code. In this case, the compiler saw that the loop was empty -- it didn't change the value of the predicate. So, it optimized it.

It moved the value from memory into a register and then spun on the register. It never reloaded the value. When the initialization function changed the value, it changed it in a different register and then stored it back to memory. Even though it was the same variable, since the two files were different, they were compilked and optimized separately from each other. The compiler never saw the relationship between the two pieces of code, so it optimized it out.

Correcting this problem was as easy as adding the "volatile" qualifier to the delaration: "volatile int ready;". "volatile" hints the compiler that the variable can be changed in ways not evident in the code. It is also used for variables whose values are set by hardware, such as I/O devices.

Two Threads, unconstrained

Below is a really simple example program. It makes two threads count: one evens, one odds. You'll notice, though, that if you watch the output, they don't interleave in a consistent way:

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


typedef struct {
  int start;
  int increment;
  int iterations;
} countargs;


void count (countargs *args) {
  int index;
  int number = args->start;
  int increment = args->increment;
  int iterations = args->iterations;

  for (index=0; index < iterations; index++) {
    printf ("%d\n", number);
    number += increment;
  }
}


int main (int argc, char *argv[]) {
  pthread_t evens;
  pthread_t odds;
  countargs args1;
  countargs args2;

  args1.start = 0;
  args1.increment = 2;
  args1.iterations = 1000;
  pthread_create (&evens, NULL, (void *)count, &args1);

  args2.start = 1;
  args2.increment = 2;
  args2.iterations = 1000;
  pthread_create (&odds, NULL, (void *)count, &args2);

  pthread_join (evens, NULL);
  pthread_join (odds, NULL);

  return 0;

  

Syncrhonization

We can use a simple mutex to force the evens and odds to group

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

pthread_mutex_t turnkeeper = PTHREAD_MUTEX_INITIALIZER;


typedef struct {
  int start;
  int increment;
  int iterations;
} countargs;


void count (countargs *args) {
  int index;
  int number = args->start;
  int increment = args->increment;
  int iterations = args->iterations;

  pthread_mutex_lock (&turnkeeper);

  for (index=0; index < iterations; index++) {
    printf ("%d\n", number);
    number += increment;
  }

  pthread_mutex_unlock (&turnkeeper);
}


int main (int argc, char *argv[]) {
  pthread_t evens;
  pthread_t odds;

  countargs args1;
  countargs args2;

  args1.start = 0;
  args1.increment = 2;
  args1.iterations = 1000;
  pthread_create (&evens, NULL, (void *)count, &args1);

  args2.start = 1;
  args2.increment = 2;
  args2.iterations = 1000;
  pthread_create (&odds, NULL, (void *)count, &args2);

  pthread_join (evens, NULL);
  pthread_join (odds, NULL);

  return 0;
}

  

Strict Alternation

The following code shows how we can use condition variables to cause one thread to poke another to force strict alternation
#include 
#include 

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t turnchange;
int eventurn = 1;


typedef struct {
  int start;
  int increment;
  int iterations;
} countargs;


void count (countargs *args) {
  int index;
  int number = args->start;
  int increment = args->increment;
  int iterations = args->iterations;

  for (index=0; index < iterations; index++) {
    pthread_mutex_lock (&mutex);
    if ( ((args->start%2==0) && !eventurn) ||
         ((args->start%2!=0) && eventurn) )
      pthread_cond_wait(&turnchange, &mutex);

    eventurn = !eventurn;

    pthread_mutex_unlock (&mutex);

    printf ("%d\n", number);
    number += increment;

    pthread_mutex_lock (&mutex);
    pthread_cond_signal(&turnchange);
    pthread_mutex_unlock (&mutex);
  }

}


int main (int argc, char *argv[]) {
  pthread_t evens;
  pthread_t odds;

  countargs args1;
  countargs args2;

  pthread_cond_init (&turnchange, NULL);

  args1.start = 0;
  args1.increment = 2;
  args1.iterations = 1000;
  pthread_create (&evens, NULL, (void *)count, &args1);

  args2.start = 1;
  args2.increment = 2;
  args2.iterations = 1000;
  pthread_create (&odds, NULL, (void *)count, &args2);

  pthread_join (evens, NULL);
  pthread_join (odds, NULL);

  return 0;
}
    

Threaded Echo Server

Below is a quick example of how we can use threads to create a concurrent server without using select. This example basically does nothing -- just echos whatever it is sent.

You can see that it is threaded by telnetting to it from multiple telnet windows at the same time. If you want to compare the result to a version without threads that is non-concurrent, you can take the echo function out of the thread and call it directly -- follow the comments.

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>

void doecho (int sessionfd) {
  char buffer[256];

  while (1) {
    int bytes = read (sessionfd, buffer, sizeof(buffer));
    if (bytes < 0)
      break;
    write (sessionfd, buffer, bytes);
  }

  close (sessionfd);
}


void handleclient (int sessionfd) {
  pthread_t tid;
  pthread_create(&tid, 0, (void*)doecho, (void *) sessionfd);

}

int main (int argc, char *argv[]) {

  int socketfd, acceptfd;
  struct sockaddr_in clientaddr;
  struct sockaddr_in serveraddr;

  socketfd = socket (AF_INET, SOCK_STREAM, 0);

  memset (&serveraddr, 0, sizeof(serveraddr));
  serveraddr.sin_family = AF_INET;
  serveraddr.sin_addr.s_addr = htonl (INADDR_ANY);
  serveraddr.sin_port = htons (9000);

  bind (socketfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
  listen (socketfd, 10);

  while (1) {
    int len = sizeof(clientaddr);
    acceptfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);

    handleclient (acceptfd); /* remove this and add code below to unthread */

    /* doecho (acceptfd); add this and remove line above to see unthreaded */
  }

  return 0;
}