CS61C Lab 14

Multicore Programming and POSIX Threads

Background

Goals

This is a very new CS 61C lab (only the second time ever)! Please be patient with any bumps in the road...

You will run this lab on the new instructional cluster, which consists of 26 compute nodes each with:

  • 2 Quad-Core Intel Xeon CPUs @ 2.33GHz (8 cores total)
  • 64+64 KiB of L1 cache
  • 4 MiB of L2 cache
  • 8 GiB of RAM
  • 10K RPM disks.

Sweet! Thanks, Google and Intel!

The purpose of this lab is to acquaint you with basic POSIX Thread (PThread) programming as well as to introduce some of the common design issues in multicore programming.

For an excellent PThread tutorial to supplement this lab, please refer to POSIX Thread Programming.

Setting Up the Files

First, log into the icluster front-end node:

$ ssh -X icluster.eecs.berkeley.edu					
				

Once logged into the icluster, create a subdirectory in your home folder called "lab14", and copy the lab files into the directory:

$ mkdir ~/lab14
$ cd ~/lab14
$ cp -R ~cs61c/labs/14/* .
				

When compiling any code with PThreads, be sure that you call gcc with the -lpthread switch:

$ gcc -o HelloWorld HelloWorld.c -lpthread					
				

Several warnings will print when you compile. Don't worry about them.

PThread Programming Exercises

Exercise 1: Hello World!

In this exercise you will write a simple Hello World program in which multiple threads print messages.

Our emphasis in this lab will not be on writing code that runs faster on multicore. Instead, it will be on writing correctly synchronized code, which means code that will produce the correct result regardless of the order that threads are scheduled to run. This is valuable not only for writing programs on parallel processors, but also for writing threaded programs for single core processors. For this reason, you are not allowed to edit or delete any of the existing lines of code in the program. You many only add lines.

First, you will need to be able to create new threads, which you can do with the following PThread API routine:

int pthread_create(pthread_t *thread, pthread_attr_t attr, void* (*start_routine)(void*), void *arg)
				

The first argument, pthread_t *thread, allows the calling thread to keep a structure that contains data relevant to the thread it creates; you can think of it as keeping a reference. We will not use the pthread_attr_t attr argument. The start_routine argument is a function pointer to the function that the thread will start in. In C, you will always pass a function name for this argument. Finally, the arg argument is a value of ambiguous type (thus the void * typing) that will be passed to the start_routine that is called when the thread begins.

A typical call to create a thread looks like:

pthread_t the_other_thread;
pthread_create(&the_other_thread, NULL, startingFunction, 1234);					
				

The call above creates and runs a new independent thread of execution which passes the value 1234 as an argument to startingFunction and begins executing that function.

Note: The implementation of pthreads we are using seems to yield control of execution to the newly spawned thread right after creating it. This is only a quirk of our implementation and is not part of the pthread standard. Remember that properly synchronized programs should make no assumptions about how threads are scheduled, so do not rely on this quirk of the implementation in your program.

Read the file HelloWorld.c and add a call to pthread_create to create threads that call the sayHello function. You will need to compile your code with the -lpthread flag. Disregard any compiler warnings.

$ gcc -o HelloWorld HelloWorld.c -lpthread					
				

You will notice that not all the threads were able to print their hello AND goodbye messages. When the original thread (the one that started with main) of a program exits, the entire process quits and thus all threads within that process are killed. In this case, the original thread creates child threads and relinquishes execution control to them because of the implementation quirk mentioned above. As soon as the original thread regains control, however, it immediately exits without waiting for any of its child threads to complete, thus killing all of them.

To make the original thread wait for its child threads to complete, we must add another loop to HelloWorld.c with a call to pthread_join, which has the following prototype:

int pthread_join(pthread_t thread, void **value_ptr);
				

When thread A joints thread B, thread A will not continue out of the call to pthread_join until thread B has completed and exited. The first argument is the pthread that the caller thread will join. We will not use the second argument.

Add a loop to HelloWorld.c so that the main thread joins each child thread before main finishes and returns. Your new program should allow all the child threads to print goodbye.

Checkoff

Show your TA the output of your HelloWorld program. 

Exercise 2: Hello World with a Shared Counter

In this exercise, you will introduce some data that is shared between the threads. Namely, you will have each thread print a number that corresponds to the order in which they print.

When you ran your program from Exercise 1, you should have noticed that the threads attempt to count off while saying goodbye, but fail miserably. You will add code to correct this. Remember not to edit or delete existing lines of code (unless you added it in Exercise 1).

First, note the global integer variable global_counter at the beginning of the file. We want each thread to increment this counter when it prints. If you try running your program again, however, you should notice that the threads don't manage to increment it more than once or twice. This is because the counter variable is shared across multiple threads without any synchronization constructs to protect it. We will need to use the mutex counter_lock to ensure that multiple threads do not access the variable at the same time and introduce race conditions.

One can lock and unlock a mutex with the functions prototyped below:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
				

Some typical lock code would be:

// as a global variable
pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;

// before a critical section corresponding to lock1
pthread_mutex_lock(&lock1);
// critical section
pthread_mutex_unlock(&lock1);
// after critical section
				

The PTHREAD_MUTEX_INITIALIZER is a macro that initializes the mutex variable to contain the necessary lock information. Mutexes must be initialized before use.

Edit the sayHello function so that the threads which call it will first try to acquire the counter_lock before incrementing the global counter variable. After doing this, each thread should number itself properly while saying goodbye.

Checkoff

Show your TA the output of your new HelloWorld program. 

Exercise 3: Hello World with a Controlled Order

Now we want to have our threads say goodbye in a specific order: we want those with an even id to print first, and those with an odd id to print after all the evens have printed. Since we are not guaranteed that the threads will start in any given order, we must have the odd threads wait until all the even threads have printed. (We won't care about the order they print hello).

Enter the dreaded condition variable (or CV for short)! A condition variable is a construct that allows threads to sleep until a certain condition is met, where the condition is determined by some shared information (such as a counter variable). CVs are always associated with locks because the shared information that they depend on must be synchronized across threads.

You can think of a CV as a "big pillow" in the sense that threads can fall asleep on a condition variable and be woken from that condition variable. One story to keep in your head is: Tommy the Thread wanted to access shared information. He acquired the appropriate lock but then was disappointed to see that the shared information wasn't ready yet. Without anything else to do, he decided to sleep (or wait) on a nearby condition variable until another thread updated the shared information and woke him up. Timmy the Thread finally came along and updated the shared information while Tommy was asleep on the nearby CV. Timmy noticed Tommy asleep, so Timmy signaled him to wake up off the condition variable. Timmy then went on his way and left Tommy to play with his new, excited shared information.

The above story sounds like a pretty useful thing for threads to be able to do, but there's one thing we glossed over: we know that to access the shared information a thread needs to hold the lock. Since Timmy needs to update the shared information while Tommy is asleep and waiting, we must have any thread that sleeps on a condition variable release the lock while it is asleep. But since Tommy had the lock when he fell asleep, he expects to still have the lock when he wakes up, and so the condition variable semantics guarantee that a thread sleeping on a condition variable will not fully wake up until (1) it receives a wake-up signal and (2) it can re-acquire the lock that it had when it fell asleep.

For more of a description (with no silly story), you can check out POSIX Thread Programming

To achieve the evens-print-first functionality, you will need to use the condition variable routines in the PThread library:

pthread_cond_wait(pthread_cond_t *condition, pthread_mutex_t *lock);
pthread_cond_broadcast(pthread_cond_t *condition);
				

The pthread_cond_wait routine will put the caller thread to sleep on the condition variable condition and release the mutex lock, guaranteeing that when the subsequent line is executed after the caller has woken up, the caller will hold lock. The pthread_cond_broadcast routine wakes up all threads that are sleeping on condition variable condition.

IMPORTANT: To call any of the above routines, the caller must first hold the lock that is associated with that condition variable. Failure to do this can lead to several problems.

Check these global variables in your HelloWorld.c file:

pthread_mutex_t print_order_lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t evens_done_CV = PTHREAD_COND_INITIALIZER;
int number_evens_finished = 0;
				

Add the logic necessary to have all the evens print before the odds. You will need to edit the sayHello function to contain calls to pthread_mutex_lock, pthread_mutex_unlock, pthread_cond_wait, and pthread_cond_broadcast. Do not use pthread_cond_signal.

Checkoff

Show your TA the output of your new HelloWorld program in which the threads with even-numbered IDs print before any threads with odd-numbered IDs. 

Exercise 4: (Extra for Experts) Implementing a Barrier

Read about barriers on Wikipedia

Barriers are not standard in the PThread library (though some implementations contain them). Implement a my_barrier_t type and associated functions.



Lab edited for CS61C Summer 2008 by Albert Chae. Originally created for CS 61C in Spring 2008 by Matt Johnson and Ramesh Sridharan.