Project 5: A user-level thread library

Due: Friday, Nov. 21st, at 11:59:59 PM.

Test files for grading

For this project, we are releasing all the test files that we will use to grade your project (Thanks to the TAs!). Hence, this is another chance for you to get a perfect score.

The test files can be obtained from: /u/a/l/aliang/CS537/p5/tests

Please read the README file first. The outputs of the test files have been confirmed with two different implementations. If you get different outputs, please study what the tests are doing. If you still have any question regarding the test files/outputs, please send an email to 537-help.

Questions

Due to a large number of questions we have received, we put important ones on a separate page.

Notes

For this project, you can work in pairs. You can choose your own partner. If you cannot find a partner, please email your instructor.

If you want to work alone, you are allowed to do so.

Objectives

There are four objectives to this assignment:

To learn how user-level threads work
To learn how those pesky condition variables really work

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. These are different than the kernel-level threads we have been talking about in-class, which are almost like processes except that they share the same address space.

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 userthread.h .

void thread_libinit()

thread_libinit is called to initialize the thread library.

int 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. Upon failure, it will return -1. Upon success, it will return an integer thread ID that can be used to later call thread_join() and thus wait for a given thread to terminate.
For each thread, please allocate 256 KB (256 * 1024 bytes) of stack area.

void thread_yield(void)

thread_yield causes the current thread to yield the CPU to the next runnable thread. The yielding thread should be put at the tail of the ready queue.

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.
A thread that calls thread_wait must first unlock the lock specified in the argument before yielding the CPU. However, when the thread is awaken and gets the CPU back, the lock must already be locked again.

int thread_join(int tid)

when this routine is called, it should wait for the given thread identified by tid to finish. Upon success, it should return zero (this includes that case, of course, where the thread is already finished). If given a bad ID or any other failure occurs, thread_join() should return -1.

Building a Dynamically-Linked Library

Your thread library needs to be built into a dynamically-linked shared library, as before. The library should be called libuserthread.so .

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 a program that shows 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

Read the man pages of getcontext(), makecontext(), and swapcontext() to learn more.

Ensuring Atomicity

To ensure atomicity of multiple operations, users will use locks that you provide. Inside of a general, preemptive system, locks are painful, thanks to interrupts that may happen at untimely times. However, in this project, we are making your life a little easier: there is no preemption; 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 test-and-set to implement locks. Think about why this is and build your locks and condition variables accordingly.

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 and a lock.

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.

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).

Other Tips

How to start: Start by implementing thread_libinit, thread_create, and thread_join. After that, try to implement 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.

Assertions: 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.

Static declarations: 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.

Sharing a folder: Here is an example how to share a folder with your partner. Make sure you run the 5th command to prevent others from reading your files. Do not put your files in your public directory.

% mkdir ~mjordan/p5 % cd ~mjordan/p5 % fs la Access list for . is Normal rights: system:administrators rlidwka system:anyuser l mjordan rlidwka % fs sa . spippen write % fs sa . system:anyuser "" % fs la Access list for . is Normal rights: system:administrators rlidwka mjordan rlidwka spippen rlidwk

Keep old versions around. Keep copies of older versions of your program around, as you may introduce bugs and not be able to easily undo them. A simple way to do this is to keep copies around, by explicitly making copies of the file at various points during development. For example, let's say you get a simple version of mysort.c working (say, that just reads in the file); type cp mysort.c mysort.v1.c to make a copy into the file mysort.v1.c . More sophisticated developers use version control systems like CVS , but we'll not get into that here (yet).

Handing in your Code

Hand in your source code (*.c and *.h), a Makefile, and a README file. Your Makefile should build the library. If your program does not work perfectly, your README file should explain what does not work and the reason (if you know).

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

Since you can work in pairs, please copy all the files only to one handin directory. For example, if mjordan and spippen work together, and they decide to submit their files in mjordan handin directory, their handin directories should look like this:

spippen: His handin directory (~cs537-SECTION/handin/spippen/p5/) should only contain partner.txt and EMPTY.txt files. The partner.txt file should contain your partner's login name on the first line (not the second or third line!). In this example, "mjordan" should be written on the first line. EMPTY.txt contains nothing; the existence of the file indicates that we should look your files in your partner's directory.

mjordan: His handin directory (~cs537-SECTION/handin/mjordan/p5/) should contain all the required submissions specified above. In addition to that, there must also be a partner.txt file which has "spippen" written on the first line.

For those of you who decide to work alone (say your login name is drodman ). Your handin directory should look like this:

drodman: Your handin directory (~cs537-SECTION/handin/drodman/p5/) should contain all the required submissions specified above. In addition to that, there must also be a blank ALONE.txt file which indicates that you work alone.

After the deadline for this project, you will be prevented from making any changes in these directory. Remember: No late projects will be accepted!