CS 537: Fall 2005

Programming Assignment 3: Your Own Thread Library


Due: TUESDAY, NOVEMBER 22nd at 10pm
You are to do this project BY YOURSELF
This project must be implemented in C

Notes

11/19: Some tests we will definitely run:
Single thread: A program that creates and thread (which prints something out) and then waits for it to complete.
Multiple threads: creates multiple threads and then waits for all of them to complete. The threads should be run in FIFO order. There may be a large number of these, too -- which could stress whether you clean up the thread stack when done.
Multiple threads + yield: creates multiple threads which frequently yield to each other. Testing whether yield works and FIFO ordering is preserved.
Multiple threads + lock: creates threads and has them call lock/unlock on a single lock around a critical section.
Multiple threads + lock + yield: creates threads, which grab lock then yield. Makes sure that lock is still held properly across yielding. Also makes sure that lock queue is managed in FIFO order.
Multiple threads + condition variable: creates two threads which use a condition variable to wait/signal each other.
Multiple threads + condition variable + yield: creates threads which use condition variables and locks to test whether condition variable queues are managed in FIFO order.
If you are able to run these tests, you will certainly get a good deal of credit for the project (there will be a few more tests though).

11/19: Set the stack size of new threads to something between 256KB and 1MB (the exact number doesn't matter).


11/18: You can assume virtually all of the tests will call thread_waitall() from the main thread after creating a bunch of threads. The main thread might call thread_yield, though, first. Other threads will not be calling thread_waitall(). The idea here is to make your life simpler -- if your library is more general and can handle these things, great.

11/18: Do NOT use the PTHREADS library in any way in this assignment.

11/8: The prototype for thread_create() has been clarified, and is also highlighted in red.

11/7: From discussion today: Simple list Basic context switching code More advanced switching Add yielding

11/4: Note that this description is slightly different than what we covered in discussion class. The differences are highlighted in red.

Objectives

There are four objectives to this assignment:
To learn how threads really work
To learn how to build a real (and fast?) mutex lock
To learn how those pesky condition variables really work
To learn how to build a shared library
In this project, you will write a library to support multiple threads within
a single Linux process. This is called a "user-level" thread library, because
the kernel doesn't know these threads exist; they are just something the
library implements and multiplexes onto a single process.

1.0 Thread Library Interface

This section describes the interface to the thread library for this project.
You will write your own implementation of this interface; you will also write
(many!) test programs that use this interface.

You should create a header file that has the following routines within it;
you should call this file "537thread.h".

void thread_libinit()

thread_libinit is called to initialize the thread library.

void thread_create(void func(void *), void *arg)

thread_create is used to create a new thread. When the newly created
thread starts, it will call the function pointed to by func and pass it the
single argument arg.
void thread_yield(void)
thread_yield causes the current thread to yield the CPU to the next
runnable thread.

void thread_lock_init(lock_t *lock)
void thread_cond_init(cond_t *cond)

void thread_lock(lock_t *lock)
void thread_unlock(lock_t *lock)
void thread_wait(lock_t *lock, cond_t *cond)
void thread_signal(cond_t *cond)
thread_lock, thread_unlock, thread_wait, and thread_signal implement locks
and condition variables. lock_t is a type you define in your thread
library and has all needed info to build a proper lock. cond_t is a type
you define and is used to implement condition variables. The associated
init functions do what is needed to init the mutex or condition variable.

int thread_waitall()

when the main thread calls this, it should wait for all other
threads to finish. If all threads run to completion, thread_waitall()
should return 0. If threads still exist but none are runnable,
thread_waitall() should return -1.

2.0 Building a Dynamically-Linked Library

Your thread library needs to be built into a dynamically-linked shared
library. In this subsection, we tell you how to do that. There are two basic
steps: first, how t o compile each .c (or .cc) file that comprises your
library, and then how to link them all together to create a shared
library. Once you have your shared library, you need to know how to compile a
program (say main.c) so that it can link with your library, so we will
describe that too. All of these things should be done within your makefile!

2.1 Compiling A Dynamically-Linked Library

Let's say you have implemented your library in a single file, called
537thread.c. This is how you should compile that within your makefile:


gcc -c 537thread.c -g -Wall -fpic

The -c flag tells the compiler to create an object file (in this case,
537thread.o), the -g flag is good to have on when debugging (so you can use
the debugger gdb), the -Wall flag should ALWAYS be used, and finally the -fpic
flag tells the compiler to use something called "position-independent" code,
which is good to use when building shared libraries. We'll learn more about
what this means later; if you are curious, read the gcc info page for details.

Note that any other files that are going to be linked into the library should
be compiled in the same way.

Now that you have 537thread.o, you'll want to make a shared library out of
it. The way you do that is with the following line:

gcc -o lib537thread.so 537thread.o -shared

It's that easy!

2.2 Linking A Program With Your Library

Let's say you have a program, main.c, that wants to use this thread library.
It should first of all include the header file, "537thread.h".
In order to compile main.c, you need to link it with your library. This is how
you would do that, assuming all of your code is in the same directory:

gcc -o main main.c -L. -R. -l537thread

The -l537thread flag tells the compiler to look for a library called
lib537thread.so (or lib537thread.a, but don't worry about that), and the
-L. flags tells the compiler to look for the library in the "." directory
("." is a way to refer to the current direc tory in Unix). Finally, the
-R. flag tells the compiler to include information in the executable that
tells the program, when running, to look in "." to find the library.

You now know how to build a shared library. However, you may not (yet) know
how to build a thread library. We now discuss some of the key components.

3.0 Creating and Swapping Threads

You will be implementing your thread library on x86 machines running the Linux
operating system, as usual. Linux provides some library calls (getcontext(),
setcontext(), makecontext(), swapcontext()) to help implement user-level
thread libraries. You will need to read the manual pages for these calls.

Here is an example of how these routines can be used:

When you run this, you get the following:

prompt> ./main2
start f2: 400
start f1: 300
finish f2: 400
finish f1: 300
prompt>
Read the man pages of getcontext(), makecontext(), and swapcontext()
to learn more.

4.0 Ensuring Atomicity

To ensure atomicity of multiple operations, users will use locks that
you provide. To implement those locks, you COULD use the "compare and
swap" routine provided by the x86. Here is a code snippet
that allows you to call compare and swap (a low-level assembly routine) from
C.


However, your thread library is NOT preemptive -- you should have
complete control over when a thread runs and hence a thread will
not be interrupted and context-switched arbitrarily to another
thread. Because of this, you do NOT need to use anything fancy like
compare-and-swap to implement locks. Think about why this is and
build your locks and condition variables accordingly.

5.0 Scheduling Order

This section describe the specific scheduling order that your thread library
should follow. Remember that a correct concurrent program must work for all
thread interleavings.

All scheduling queues should be FIFO. This includes the ready queue and the
queue of threads waiting for a signal.

When a thread calls thread_create, the caller does not yield the CPU. The
newly created thread is put on the ready queue but is not executed right away.

When a thread calls thread_unlock, the caller does not yield the CPU. The
woken thread is put on the ready queue but is not executed right away.

When a thread calls thread_signal, the caller does not yield the CPU. A
woken thread is put on the ready queue but is not executed right away. A
woken thread should request the lock when it next runs.

When the main thread calls thread_waitall, it should give up the CPU
and the next runnable thread should run. If there are no threads remaining,
it should return 0. If there are no runnable threads, it should return -1.

6.0 Deleting a Thread and Exiting the Program

A thread finishes when it returns from the function that was specified in
thread_create. Remember to de-allocate the memory used for the thread's stack
space and context (do this AFTER the thread is really done using it).

7.0 Other Tips

Start by implementing thread_libinit, thread_create, and thread_yield. Don't
worry at first about locking and condition variables. After you get that
working, worry about locks. Get those working, test them thoroughly.
After that, get condition variables working.

Use assertion statements copiously in your thread library to check for
unexpected conditions. These error checks are essential in debugging
concurrent programs, because they help flag error conditions early.
Read the man page for assert() to see how these work.

Declare all internal variables and functions (those that are not called
by clients of the library) "static" to prevent naming conflicts with programs
that link with your thread library.

Your thread library must compile into a single dynamically-linked shared
library called "lib537thread.so", with an associated header file "537thread.h".