Due: 10:30 pm Friday 13 December 2019
Summary: You will implement locking and parallel programs
Collaboration: You will work during lab in randomly assigned pairs. You must complete the work together in these groups, whether during or after the scheduled lab time.
Submitting: Submit your bank.c, map.c and references.txt to the appropriate Assignment in PioneerWeb. Be sure to follow the submission guidelines and use precisely these file names.
In this lab, you will practice using threads and locks to complete the implementation of two simple parallel programs. Both programs include test drivers that will help you determine whether your implementation is working correctly. There are some additional requirements that are not checked by the test programs, so please read the instructions carefully.
All the starter code is available in parallelism.tar.gz. Download and unpack this archive to set up for the lab.
The POSIX library contains a wide variety of portable routines for managing concurrency. While these are covered and utilized in far more detail in CSC 213, it’s still important to gain some facility with these notions, even if you do not end up taking that class. To that end, we have created a suite of routines that provide both locking and thread-launching, both of which you’ll need for this lab.
The “library” (a bit of a misnomer) is a header-only library, meaning that instead of compiling a separate object file and linking with it, you merely need to include the appropriate header, which contains all the code (both declaration and implementation) for a set of procedures that nicely wrap the underlying pthreads library.
Good, defensive C programming generally requires you to check the return codes from system or library calls and take action accordingly. The main utility of these wrapper procedures is they both exclude arguments you won’t need to worry about and handle any errors for you by reporting them and exiting your entire program.
To use the “mythreads” library all you need to do is #include "../mythreads.h" in your source files. (In fact, the starter files already have this for you, so you don’t need to do anything special other than call the functions we lay out for you below.)
In parallel programs, a lock is used to protect what is often called a critical section. Because we want access to a critical section of code to be mutually exclusive (that is, only one thread should be running it at a time), the lock is called a mutex.
Using a lock (or mutex) involves four steps:
The following code snippet demonstrates how to do this in the “mythreads.h” library:
pthread_mutex_t deadbolt; // 1. Declare the lock variable
mutex_init (&deadbolt); // 2. Initialize the lock variable
// ...
mutex_lock (&deadbolt); // 3. Attempt to lock the variable
// Here is the critical section
mutex_unlock (&deadbolt); // 4. Unlock the mutexNote that you should generally name the mutex variable something appropriate to the application context.
Creating a new thread of computation in a shared memory system allows these multiple computations to proceed concurrently (and perhaps in parallel if the host system has multiple processors).
Managing threads requires attention to two additional details:
Recall that function pointers in C allow us to pass references to procedures we want to execute, much like functions such as map and reduce in Scheme do. Launching a thread involves giving both the name of the procedure (which must both take a void * argument as a parameter and produce a void * as its return type), and the value of the parameter the procedure should be given when it starts running in the new thread.
The following C function meets these criteria.
/* Function that expects a pointer to a single character */
void *
worker (void* arg)
{
printf ("Received %c", *(char*)arg);
return NULL;
}This function takes a pointer to any type of value, so before we use that pointer (e.g., by dereferencing it), we need to cast it to the type we expect to have gotten (in this case, a char *, or a pointer to a single character). Note that this worker function does not produce a value, so we safely return the pointer NULL because we do not expect any one to make use of it.
Launching three separate threads to run this simple worker would look like the following.
pthread_t thread1, thread2, thread3; // Declare the thread variable(s)
char arg1, arg2, arg3;
arg1='A';
arg2='B';
arg3='C';
thread_create (&thread1, worker, &arg1);
thread_create (&thread2, worker, &arg2);
thread_create (&thread3, worker, &arg3);If you ran this code several times, you would perhaps see the characters A, B, and C printed in different orderings, depending on how the threads were scheduled to the processors.
Be careful that you have created separate argument values! The following approach creates a problematic race condition. Make sure you understand why before you move on.
pthread_t thread1, thread2, thread3; // Declare the thread variable(s)
char arg;
arg='A';
thread_create (&thread1, worker, &arg);
arg='B';
thread_create (&thread2, worker, &arg);
arg='C';
thread_create (&thread3, worker, &arg);structsIf we need to pass multiple parameters to a thread function, these are usually packed together into a struct. For example:
To minimize casting and compiler warnings, the thread worker function could then extract this type from the void* argument once at the outset:
/* Function that expects a pointer to an alphanum_t parameter type */
void *
worker (void* arg)
{
alphanum_t * args = (alphanum_t*)arg;
printf ("Received %c/%d", args->letter, args->number);
return NULL;
}Launching the thread with such a parameter proceeds as one might expect:
alphanum_t arg1; // Declare argument variable
arg1.letter = 'A'; // Initialize argument values
arg1.number = 42;
pthread_t thread1; // Declare a thread variable
thread_create(&thread1, worker, &arg1); // Launch the thread with the argumentIf the launch above was all there was to your program, the thread running main might actually finish before all of the younger threads completed, leading to some unexpected behavior. Therefore, many multi-threaded programs also ask the main thread to block further execution until your worker threads have completed. This can be done in the “mythreads” library with the thread_join function:
thread_join (thread1); // Will not return until thread1 is complete
thread_join (thread2); // Will not return until thread2 is complete
thread_join (thread3); // Will not return until thread3 is completeBear in mind that thread 3 may have completed before thread 1. In that case, the last call to thread_join will return immediately, not having to wait for completion. Note that these procedures do not take pointers, but rather the actual thread values themselves.
The bank program implements a simple banking simulation. The main bank implementation is in bank.c; this is where you will make all of your changes. The simple simulation allows users to create accounts, add transactions, transfer money between accounts, and access both the transaction count and current balance for each account. Each account is accessed from a different thread.
Your task for this part of the lab is to fix the bank simulation. If you run the test program using the command make test, you will see that the banking system tends to lose money and transaction counts. This is because the bank simulation does not use any synchronization operations. You should add locks to the banking system to make sure it does not lose any transactions. However, you may not use a single lock to protect the whole bank! You must allow independent transactions on separate accounts to proceed in parallel.
Hint: To avoid deadlock between multiple threads trying to acquire the same set of locks, you should ensure they attempt to acquire the locks in the same order.
Please do not change anything in the bank-test.c and bank.h files. You are only required to submit bank.c for this part. It will be tested it with the original versions of the other two files.
For the second part of the lab, you will implement a parallel version of the familiar map function from Scheme. The starter code for this part includes a serial implementation, along with a test program that will run your parallel implementation with 1, 2, 4, and 8 threads, and validate all the results.
Your job is to implement parallel_map. The output from the function must be correct, but you must also adhere to a few additional requirements:
threads threads in the function. That means you will need to have each thread run the mapped function over more than one entry.parallel_map. A more advanced implementation might have the main thread do work as well, but please do not do that for this lab.Copyright © 2018, 2019 Charlie Curtsinger and Jerod Weinman
This work is licensed under a Creative Commons Attribution NonCommercial ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA.