Project Goals
The goals of this project are:
- to enable multiple threads to safely coexist
- to extend our basic thread library with important synchronization primitives
Administrative Information
The project is an individual project. It is due
on Tuesday, May 14, 2019, 23:59:59 PST (no
deadline extensions or late turn ins).
Adding synchronization to your Linux thread library
In the previous project, we have seen how we can start threads,
let them perform computations, and then exit. However, we have
not considered (intentional or unintentional) interactions
between threads. In this project, we want to address this issue
and add support so that multiple threads can safely co-exist.
The project consists of three parts:
-
First, you have to add a basic locking mechanism to your thread
library. For this, you have to implement the functions void
lock() and void unlock(). Whenever a thread
calls lock, it can no longer be interrupted by any
other thread. Once it calls unlock, the thread resumes
its normal status and will be scheduled whenever an alarm signal
is received. Having the lock and unlock
functions available is useful to protect small pieces of code
(critical sections) during which you do not want that the
current thread is interrupted. For example, lock
and unlock should be used whenever your thread library
manipulates global data structures (such as the list of running
threads). A thread is supposed to call unlock only when
it has previously performed a lock operation. Moreover,
the result of calling lock twice without invoking an
intermediate unlock is undefined.
-
Second, you have to implement the following POSIX
thread function:
int pthread_join(pthread_t thread, void **value_ptr);
The pthread_join() function shall suspend execution of
the calling thread until the target thread terminates, unless
the target thread has already terminated. On return from a
successful pthread_join() call with a
non-NULL value_ptr argument, the value passed
to pthread_exit() by the terminating thread shall be
made available in the location referenced
by value_ptr. When a pthread_join() returns
successfully, the target thread has been terminated. The results
of multiple simultaneous calls to pthread_join()
specifying the same target thread are undefined.
As you can see, with pthread_join, there is now the
need to correctly handle the exit code of threads that
terminate.
-
Third, you are supposed to add semaphore
support to your library. As discussed in class, semaphores are a
useful tool to allow multiple threads to coordinate their
actions. We will implement the following functions (for which
you should include semaphore.h):
int sem_init(sem_t *sem, int pshared, unsigned value);
The sem_init() function shall initialize the unnamed
semaphore referred to by sem. The value of the
initialized semaphore shall be value. The value has to
be less that SEM_VALUE_MAX, which is 65,536 for this
project. The pshared argument has to be always zero,
which means that the semaphore is shared between threads of the
process. Attempting to initialize an already initialized
semaphore results in undefined behavior.
int sem_destroy(sem_t *sem);
sem_destroy() destroys the unnamed semaphore at the
address pointed to by sem. Only a semaphore that has
been initialized by sem_init should be destroyed
using sem_destroy. Destroying a semaphore that other
threads are currently blocked on (in sem_wait)
produces undefined behavior. Using a semaphore that has been
destroyed produces undefined results, until the semaphore has
been reinitialized using sem_init.
int sem_wait(sem_t *sem);
sem_wait decrements (locks) the semaphore pointed to
by sem. If the semaphore’s value is greater than
zero, then the decrement proceeds, and the function returns
immediately. If the semaphore currently has the value zero,
then the call blocks until it becomes possible to perform the
decrement (i.e., the semaphore value rises above zero). Note
that in this implementation, the value of the
semaphore never falls below zero (unlike
how it was shown in class).
int sem_post(sem_t *sem);
sem_post() increments (unlocks) the semaphore pointed
to by sem. If the semaphore’s value consequently
becomes greater than zero, then another thread blocked in
a sem_wait call will be woken up and proceeds to lock
the semaphore. Note that when a thread is woken up and takes the
lock as part of sem_post, the value of the semaphore
will remain zero.
Implementation
In this project, you will build upon the thread library that you
developed for the previous project. Thus, as a first step, make
sure that everything works well. Then, copy your code from
Project 2 into the new directory and extend your library as
outlined above.
Adding the lock and the unlock functions is
straightforward. To lock, you just need a way to make sure that
the currently running thread can no longer be interrupted by an
alarm signal. For this, you can make use of
the sigprocmask function. To unlock, simply re-enable
(unblock) the alarm signal, again
using sigprocmask. Once you have lock
and unlock, use them to protect all accesses to global
data structures. You will definitely need to call these
functions when implementing the semaphore routines.
To implement the pthread_join function, you will probably
need to introduce a BLOCKED status for your threads. Whenever a
thread is blocked, it cannot be selected by the scheduler. A
thread becomes blocked when it attempts to join a thread that is
still running.
In addition to blocking threads that wait for (attempt to join)
active processes, you might also need to
modify pthread_exit. In particular, whenever a thread
exits, you cannot immediately clean up everything that is related to
it. Instead, you need to retain its return value, because other
threads might later want to get this return value by
calling pthread_join. That is, once a thread exits, it
is not immediately gone, but becomes a "zombie" thread (very
similar to the situation with processes). Once a thread's exit
value is collected via a call to pthread_join, you can free all
resources related to this thread.
One question that might arise is how you can obtain the return
(exit) value of a thread that does not
call pthread_exit explicitly. We know that, in this
case, we have to use the return value of the thread's start
function (man pthread_exit). This should be fairly easy
if you have used a wrapper function for the previous
project. Since you explicitly called the start function from the
wrapper, you can simply obtain (and then process) its return
value once the start function returns.
When implementing semaphores, recall that you should first
include the appropriate header file semaphore.h. This
file includes /usr/include/bits/semaphore.h, where you
can find the definition of the type sem_t. You will
notice that this struct/union is likely not sufficient to store
all relevant information for your semaphores. Thus, you might
want to create your own semaphore structure that stores the
current value, a pointer to a queue for threats that are
waiting, and a flag that indicates whether the semaphore is
initialized. Then, you can use one of the fields of the
sem_t struct (for example, __align) to hold a
reference to your own semaphore.
Once you have your semaphore data structure, just go ahead and
implement the semaphore functions as described above. Make sure
that you test a scenario where a semaphore is initialized with a
value of 1, and multiple threads use this semaphore to
manage access to a critical code section that requires mutual
exclusion (i.e., they invoke sem_wait before entering
the critical section). When using your
semaphore, only a single thread should be in the critical
section at any time. The other threads need to wait
until the first thread exits and calls sem_post. At
this point, the next thread can proceed.
Deliverables
Please follow the instructions below exactly!
-
We
use gradescope to
manage your project submissions and to communicate the results
back to you. You will submit all files that are part of your project via
the gradescope web interface.
-
All your files must be in a directory named sync. The
name of the threads library that we will test must
be threads.o, and the POSIX function implementation
must be done in C/C++. Of course, you cannot leverage any of
the existing pthread library code to implement your thread
library.
-
All files that you need to build your library must be included
(sources, headers, makefile) in that folder. We will just call
make and expect that the object file threads.o is
built from your sources. Please do not
include any object or executable files.
-
Gradescope does support built-in autograding, but, currently,
we do not intend to use it. Instead, we will test your
projects in our own environment. So, do not worry if you don't
get immediate feedback or if the system tells you that the
autograder is not running.
-
Your project must compile on a CSIL machine. If you worked
on a Windows machine or your laptop at home, then make sure
it still works on CSIL or modify it appropriately!
-
Include a README with this project. Explain what you did in
the README. If you had problems, tell us why and what.