Week 10 Laboratory Exercises

Objectives

  • practice using POSIX threads in C
  • learn how to fix broken concurrent code
  • write a higher complexity concurrent program

Preparation

Before the lab you should re-read the relevant lecture slides and their accompanying examples.

Getting Started

Set up for the lab by creating a new directory called lab10 and changing to this directory.
mkdir lab10
cd lab10

There are some provided files for this lab which you can fetch with this command:

1521 fetch lab10

If you're not working at CSE, you can download the provided files as a zip file or a tar file.

Exercise — individual:
Compile C Files

You have been given compile_c_files.c, a C program that given the pathname of C source code files compiles them with DCC. For example:
dcc compile_c_files.c -o compile_c_files
ls -1
compile_c_files
compile_c_files.c
file_modes.c
file_sizes.c
./compile_c_files file_sizes.c file_modes.c
running the command: "/usr/local/bin/dcc file_modes.c -o file_modes"
running the command: "/usr/local/bin/dcc file_sizes.c -o file_sizes"
ls -1
compile_c_files
compile_c_files.c
file_modes
file_modes.c
file_sizes
file_sizes.c
file file_modes
file_modes: ELF 64-bit LSB executable # ...

When you think your program is working, you can use autotest to run some simple automated tests:

1521 autotest compile_c_files 

When you are finished working on this exercise, you must submit your work by running give:

give cs1521 lab10_compile_c_files compile_c_files.c

You must run give before Monday 18 November 12:00 (midday) (2024-11-18 12:00:00) to obtain the marks for this lab exercise. Note that this is an individual exercise, the work you submit with give must be entirely your own.

Exercise — individual:
Fix a data race

Provided for you is a file, fix_race.c that contains a global variable: global_counter, and a function perform_increment:

int global_counter = 0;

void perform_increment(void) {
   global_counter = global_counter + 1;
}

Unfortunately, perform_increment is currently not safe to call from multiple threads simultaneously! As it stands, this causes a data race, resulting in our program not printing the desired output:

make fix_race
dcc -pthread fix_race.c test_fix_race.c -o fix_race

should output "Global counter result: 50000000"
./fix_race 50 1000000
Global counter result: 49851959
./fix_race 50 1000000
Global counter result: 49787244
./fix_race 50 1000000
Global counter result: 50000000
./fix_race 50 1000000
Global counter result: 49285021
./fix_race 50 1000000
Global counter result: 49659783

The ./fix_race 50 1000000 specifies that 50 threads will each call perform_increment 1000000 times.

Your task is to fix the program by making the perform_increment function thread-safe to call (i.e. it can be called by multiple threads simultaneously and maintain correct program behaviour).

There are two predominant approaches to solving this exercise that we have explored in this course. Firstly, you could use a mutex (pthread_mutex_t) to guard access to the global_counter. Secondly, you could modify the global_counter to be an atomic variable, and ensure you increment it atomically.

Both of these approaches are perfectly valid -- and in-fact, we strongly suggest that you solve this exercise twice, trying each approach. Note that there is only a single submission for this exercise, so in that case you will have to choose which approach to submit.

An example of using a mutex can be found in the bank_account_mutex.c lecture code.

An example of using atomics can be found in the bank_account_atomic.c lecture code.

When your program is working, it should behave as follows:

make fix_race
dcc -pthread fix_race.c test_fix_race.c -o fix_race
./fix_race 50 1000000
Global counter result: 50000000
./fix_race 50 1000000
Global counter result: 50000000
./fix_race 50 1000000
Global counter result: 50000000
./fix_race 50 1000000
Global counter result: 50000000
./fix_race 50 1000000
Global counter result: 50000000
./fix_race 50 1000000
Global counter result: 50000000

Use make(1) to build your code:

make    # or 'make fix_race'

When you think your program is working, you can use autotest to run some simple automated tests:

1521 autotest fix_race 

When you are finished working on this exercise, you must submit your work by running give:

give cs1521 lab10_fix_race fix_race.c

You must run give before Monday 18 November 12:00 (midday) (2024-11-18 12:00:00) to obtain the marks for this lab exercise. Note that this is an individual exercise, the work you submit with give must be entirely your own.

Exercise — individual:
Fix a deadlock

Provided for you is a file, fix_deadlock.c that contains 6 functions executed as threads:

  • alice_withdraw
  • bob_withdraw
  • alice_deposit
  • bob_deposit
  • alice_send_bob
  • bob_send_alice

Each of these threads modifies 2 of 3 global variables, each protected by their own mutex. These global variables (and their accompanying mutexes) are:

  • int bank_account (guarded by: pthread_mutex_t bank_account_mutex)
  • int alice_wallet (guarded by: pthread_mutex_t alice_wallet_mutex)
  • int bob_wallet (guarded by: pthread_mutex_t bob_wallet_mutex)

However, this program has a problem: it (almost always) deadlocks! Although there are many different avenues for this program to deadlock currently, we can examine a single example of how it may be possible.

Consider the case where alice_withdraw and alice_deposit are executing in parallel. Let's say alice_withdraw manages to lock the bank_account_mutex right at the same time as alice_deposit manages to lock the alice_wallet_mutex (these are lines 29 and 65 respectively). Following onto the next line of code for each thread, we see that alice_withdraw then tries to lock the alice_wallet_mutex, which it will not be able to since we just established that the alice_deposit function just locked that mutex. However, alice_deposit's next line of code will attempt to lock the bank_account_mutex, which alice_withdraw is currently holding!

In this scenario, both threads are stuck waiting for each other's mutexes, but because they're both stuck (and cannot progress), each will never be able to unlock their own mutexes in order to allow the other thread to lock it and make further progress! This is a classic deadlock scenario!

Your task is to fix the program by establishing a global ordering of lock acquisition! Curiously enough, we have already worked through the process of fixing a (slightly simpler) deadlock scenario during lectures

An example of using a mutex incorrectly can be found in the bank_account_broken.c lecture code.

An example of using a mutex correctly can be found in the bank_account_mutex.c lecture code.

You should not solve this exercise by attempting to modify the behaviours of threads, or by making some threads do nothing, or by changing the types of any global variables, or by removing or introducing any new ones.

You should simply swap the ordering of some mutex lock and unlocks such that a global ordering is established, which will free your program from deadlock. Once this is achieved, (assuming you didn't violate any of the above conditions), your task is complete!

When your program is working, it should behave as follows:

make fix_deadlock
dcc -pthread fix_deadlock.c test_fix_deadlock.c -o fix_deadlock
./fix_deadlock
bank account balance: $10000.00
alice wallet balance: $200.00
bob   wallet balance: $200.00

Use make(1) to build your code:

make    # or 'make fix_deadlock'

When you think your program is working, you can use autotest to run some simple automated tests:

1521 autotest fix_deadlock 

When you are finished working on this exercise, you must submit your work by running give:

give cs1521 lab10_fix_deadlock fix_deadlock.c

You must run give before Monday 18 November 12:00 (midday) (2024-11-18 12:00:00) to obtain the marks for this lab exercise. Note that this is an individual exercise, the work you submit with give must be entirely your own.

Exercise — individual:
Create a chain of threads

Provided for you is a file, thread_chain.c that spawns a new thread: my_thread, which calls a provided function thread_hello() before terminating.

#include <pthread.h>
#include "thread_chain.h"

void *my_thread(void *data) {
    thread_hello();
    
    return NULL;
}

void my_main(void) {
    pthread_t thread_handle;
    pthread_create(&thread_handle, NULL, my_thread, NULL);

    pthread_join(thread_handle, NULL);
}

When we run the program, we are greeted with the following output:

make thread_chain
dcc -pthread thread_chain.c test_thread_chain.c -o thread_chain -Wl,--wrap=pthread_create
./thread_chain
Hello from thread 01!
ERROR: thread_hello was called less than 50 times!
help: it was only called 1 time(s)
the program will now exit

Your task is to make this program generate a chain of 50 threads that each say hello! Your my_main should continue to simply create one thread, however the thread it creates (my_thread) should modify its behaviour as follows:

It should continue to first call the thread_hello function. thread_hello will print the Hello from thread N! message for you as required -- do not try to manually print this message yourself as you will lose marks. It should then create another thread which continues this same process.

Similiarly to how we wrote recursive functions in the first week of COMP1521, you should make the thread create another thread that uses the same my_thread function as its entry point. Finally, it should call pthread_join on the newly created thread, in order to make sure our current thread waits for the new thread to finish!

This however, would cause an endless chain of threads spawning threads spawning threads, ad infinitum! In this exercise, you are required to make the chain have exactly length 50.

The following is a diagram to help illustrate what this chain conceptually looks like:

When your program is working, it should behave as follows:

make thread_chain
dcc -pthread thread_chain.c test_thread_chain.c -o thread_chain -Wl,--wrap=pthread_create
./thread_chain
Hello from thread 01!
Hello from thread 02!
Hello from thread 03!
Hello from thread 04!
[ ... output redacted ... ]
Hello from thread 47!
Hello from thread 48!
Hello from thread 49!
Hello from thread 50!

Use make(1) to build your code:

make    # or 'make thread_chain'

When you think your program is working, you can use autotest to run some simple automated tests:

1521 autotest thread_chain 

When you are finished working on this exercise, you must submit your work by running give:

give cs1521 lab10_thread_chain thread_chain.c

You must run give before Monday 18 November 12:00 (midday) (2024-11-18 12:00:00) to obtain the marks for this lab exercise. Note that this is an individual exercise, the work you submit with give must be entirely your own.

Challenge Exercise — individual:
Compile C Files If Needed

Copy your code from the last activity into compile_if_needed.c

cp compile_c_files.c compile_if_needed.c

Unfortunately the code you have written so far is inefficient.

It recompiles the C file even if this is not necessary.

Modify compile_if_needed.c so that it only compiles C files if necessary.

A compilation is necessary if the binary doesn't exist.

A compilation is also necessary if the C file has been changed since the last compilation. In other words, if the modification time of the C file is more recent than the modification time of the binary.

Follow the output format below.

./compile_if_needed file_sizes.c file_modes.c
running the command: "/usr/local/bin/dcc file_modes.c -o file_modes"
running the command: "/usr/local/bin/dcc file_sizes.c -o file_sizes"
./compile_if_needed file_sizes.c file_modes.c
file_modes.c does not need compiling
file_sizes.c does not need compiling
rm file_sizes
./compile_if_needed file_sizes.c file_modes.c
file_modes.c does not need compiling
running the command: "/usr/local/bin/dcc file_sizes.c -o file_sizes"
echo >> file_sizes.c # add a new-line to file_sizes.c
./compile_if_needed file_sizes.c file_modes.c
file_modes.c does not need compiling
running the command: "/usr/local/bin/dcc file_sizes.c -o file_sizes"
./compile_if_needed file_sizes.c file_modes.c
file_modes.c does not need compiling
file_sizes.c does not need compiling
./compile_if_needed file_sizes.c file_modes.c
file_modes.c does not need compiling
file_sizes.c does not need compiling

When you think your program is working, you can use autotest to run some simple automated tests:

1521 autotest compile_if_needed 

When you are finished working on this exercise, you must submit your work by running give:

give cs1521 lab10_compile_if_needed compile_if_needed.c

You must run give before Monday 18 November 12:00 (midday) (2024-11-18 12:00:00) to obtain the marks for this lab exercise. Note that this is an individual exercise, the work you submit with give must be entirely your own.

Submission

When you are finished each exercises make sure you submit your work by running give.

You can run give multiple times. Only your last submission will be marked.

Don't submit any exercises you haven't attempted.

If you are working at home, you may find it more convenient to upload your work via give's web interface.

Remember you have until Week 11 Monday 12:00:00 (midday) to submit your work without receiving a late penalty.

You cannot obtain marks by e-mailing your code to tutors or lecturers.

You check the files you have submitted here.

Automarking will be run by the lecturer several days after the submission deadline, using test cases different to those autotest runs for you. (Hint: do your own testing as well as running autotest.)

After automarking is run by the lecturer you can view your results here. The resulting mark will also be available via give's web interface.

Lab Marks

When all components of a lab are automarked you should be able to view the the marks via give's web interface or by running this command on a CSE machine:

1521 classrun -sturec