What the exam covers
Exam 2 is on paper Thursday, November 19. Expect a format like Exam 1’s: multiple-choice questions, then short answers, tracing, and finding the problems in short pieces of code. A one-page Pthreads and OpenMP reference will be supplied, so you do not need to memorize argument lists. Focus on what each construct does, which threads take part, and which data is shared. This guide is a close map of the assessed material, but its practice examples are different from the exam’s.
Review Modules 8–15, along with Projects 3 and 4. You can use the sections below as your main checklist.
Pthreads
| Function | What to remember |
|---|---|
pthread_create |
Starts a thread in a function of the form void *f(void *argument), passing it one pointer. |
pthread_join |
Waits for that thread to finish. Join before reading a worker’s results. |
pthread_mutex_lock / unlock |
Only one thread at a time holds the mutex. Every thread that updates the shared data must use the same mutex. |
pthread_cond_wait |
Called while holding the mutex. It releases the mutex while waiting and reacquires it before returning. |
pthread_cond_signal / broadcast |
Wakes one waiter, or all current waiters. It does not copy data. |
pthread_barrier_wait |
No participant continues until all of them have arrived. The count given to pthread_barrier_init is the number of threads that will call it. |
- Pass each thread a stable ID, using an array such as
ids[t]. Passing&t, the address of the changing loop variable, is a bug. - A race condition happens when threads update shared data without coordination and the result depends on timing.
total++is a read, an add, and a write, so two threads can read the same old value, and one update is lost. Joining afterward does not fix that. - A critical section is code that only one thread at a time should run. Keep it short. Do most of the work in local variables, then merge once under the lock, as in Project 3.
- Wait for a condition in a
whileloop:while (!ready) pthread_cond_wait(&cond, &lock);. When a thread wakes up, the condition might not hold, so it must check again. The shared variable, not the signal, remembers that something is ready. - In the bounded buffer, the producer waits while the buffer is full, and the consumer waits while it is empty. Both use the same mutex.
- Use a barrier when every thread must finish one phase before any thread starts the next. Without the barrier, a fast thread can read data that another thread is still writing.
OpenMP basics
# pragma omp parallelruns the next block on a team of threads. The thread that reaches the directive becomes thread 0 of the team.num_threads(t)requests a team size. At the end of the block there is an implicit barrier, and then only the original thread continues.omp_get_thread_num()gives a thread’s number, andomp_get_num_threads()gives the team size.omp_get_wtime()returns elapsed time in seconds, likeMPI_Wtime().- A variable declared before a parallel block is shared. A variable declared inside it is private. The loop variable of a
parallel foris private automatically. private(x)gives each thread its own uninitialized copy ofx. The value from before the block is not copied in or back out.default(none)makes the compiler require a scope for every outside variable the block uses.reduction(+: sum)gives each thread a private copy starting at 0, then combines the copies and the original value at the end. Other operators include*,max, andmin.
Parallel loops and dependences
# pragma omp parallel forstarts a team and divides the loop’s iterations among it. The loop must be in canonical form, so OpenMP can count the iterations before the loop starts. A loop that canbreakorreturnearly cannot use it.- A loop-carried dependence means one iteration uses a value that a different iteration writes, as in
fibo[i] = fibo[i-1] + fibo[i-2]. The compiler does not check for these. A dependence inside a single iteration is fine. - To check a loop, look at each variable the body writes. Is it read or written in a different iteration? If it is a total, a
reductionmay fix it. If it is a temporary, making it private may fix it. - In the π series,
factor = -factorwas a loop-carried dependence. Computing the sign fromkremoved it, and the sharedfactoralso had to become private.
Reusing threads and scheduling loops
# pragma omp fordivides a loop among an existing team. It starts no threads. Inside aparallelblock, every thread runs the code outside thefor, such as the outerphaseloop of odd-even sort.parallel forandforboth end with an implicit barrier.nowaitremoves it. That is safe only if nothing after the loop depends on other threads’ iterations. In odd-even sort,nowaitlets phases overlap and breaks the sort.schedule(static, c)deals out chunks ofciterations in turn before the loop starts. Withoutc, each thread gets about one equal block.schedule(dynamic, c)hands out chunks while the loop runs, to whichever thread asks next. The default chunk size is 1.guidedis similar, but the chunks shrink.schedule(runtime)reads the schedule fromOMP_SCHEDULE, as inOMP_SCHEDULE="static,1" ./program 4.- Equal numbers of iterations are not equal work when iterations cost different amounts. The load imbalance is the largest thread’s work divided by the average. With \(t\) threads and imbalance \(I\), the best possible speedup is about \(t/I\).
- Dynamic schedules cost more to run. Use them when iteration costs are uneven or unpredictable, not when every iteration is cheap and equal.
OpenMP synchronization
| Tool | Use it when |
|---|---|
reduction |
Each thread can work privately and combine once at the end. This is usually fastest. |
# pragma omp atomic |
You need to protect a single update of one variable, such as x++ or x += expression, where the expression does not use x. Only the update of x is protected. |
# pragma omp critical |
You need to protect a block of code. All unnamed critical sections share one lock. Use critical(name) to keep unrelated sections apart. |
omp_lock_t locks |
You need to protect pieces of data chosen while the program runs, such as one bucket per lock. |
# pragma omp barrier |
Every thread in the team must arrive before any continues. Every thread must reach the same barriers. |
- The OpenMP lock functions correspond to Pthreads mutex functions:
omp_init_lock,omp_set_lock,omp_unset_lock, andomp_destroy_lock. - Deadlock: a thread waits forever for a lock that it, or another waiting thread, holds. Two common causes are entering an unnamed critical section from inside another one, and two threads taking the same two locks in opposite orders.
- A correct-looking answer does not prove a program is correct. A race can give the right answer on some runs, or at some optimization levels.
Measuring performance
- Speedup is the 1-thread time divided by the parallel time, for the same work, as in Exam 1.
- Time optimized code (
-O2), and repeat runs. Keep the input, the timed region, and the node the same, and check that the answers agree. - Other programs on the same node can slow yours down.
uptimeshows how busy a node is.
Practice on paper
Try these before opening the answers.
Write commands to compile
sum.cas a Pthreads program and as an OpenMP program, with warnings and-O2. How do you run each with four threads?Two threads each run
total++once, starting fromtotal = 5. Give one order of reads and writes that ends withtotal = 6. What is the correct result, and what one change to the threads’ code makes it reliable?Find two problems in this program. It compiles. Then fix them.
int tid; double total = 0.0; # pragma omp parallel num_threads(4) { tid = omp_get_thread_num(); # pragma omp for for (int i = 0; i < 1000; i++) { total = total + i; } printf("Thread %d is done\n", tid); } printf("Total: %f\n", total);A consumer waits with
if (!ready) pthread_cond_wait(&cond, &lock);. Why should theifbe awhile? Why must the consumer holdlockbefore it calls wait?A barrier is initialized for 4 participants, but only 3 workers ever call
pthread_barrier_wait. What happens?Give the scope of
n,x,sum,i, andy, and the final value ofsum:int n = 100; double x = 2.0, sum = 0.0; # pragma omp parallel for num_threads(4) reduction(+: sum) for (int i = 0; i < n; i++) { double y = x * i; sum += y; }Can
# pragma omp parallel forbe placed safely on each loop, as written? If not, can a clause fix it?for (i = 0; i < n; i++) a[i] = a[i] + b[i];for (i = 1; i < n; i++) a[i] = 2 * a[i - 1];for (i = 0; i < n; i++) if (a[i] > big) big = a[i];for (i = 0; i < n; i++) { if (a[i] == key) break; }
A loop has 10 iterations, numbered 0 through 9, and two threads. List each thread’s iterations under
schedule(static)and underschedule(static, 3). Underschedule(dynamic), can you predict the assignment?With a block schedule, four threads do 10, 30, 30, and 10 units of work. Find the load imbalance and the best possible speedup. Name one schedule that might do better.
Which updates can use
# pragma omp atomic? (a)hits++;(b)x = x * x;(c)total += f(i);, wherefdoes not usetotal. In (c), is the call tofprotected?Thread A sets lock 1 and then lock 2. Thread B sets lock 2 and then lock 1. What can go wrong, and how do you prevent it?
In odd-even sort with one team and two
fordirectives, why must theforloops keep their implicit barriers? How many times does each thread run the outerphaseloop?A program takes 12.0 s with 1 thread and 4.0 s with 4 threads. What is the speedup? Name two things to check before trusting the comparison.
gcc -Wall -O2 -pthread -o sum sum.candgcc -Wall -O2 -fopenmp -o sum sum.c. Run each directly as./sum. The thread count comes from the program or its arguments, such as./sum 4, not frommpirun.- Both threads read 5, both add 1, and both write 6. The correct result is 7. Protect the increment with one shared mutex, so each read, add, and write happens as a unit.
tidis shared, so threads overwrite each other’s number. Declare it inside the block, or addprivate(tid).total = total + iis an unprotected update of a shared variable. Addreduction(+: total)to thefordirective. (criticaloratomicwould also be correct, but slower.)- When the consumer wakes,
readymight still be false, so it must check again before going on. Wait must be called while holding the mutex, because it releases that mutex while waiting and reacquires it before returning. That lets the producer changereadysafely. - The three workers wait forever. The barrier never gets its fourth arrival.
nandxare shared,sumis a reduction variable, andiandyare private.sum\(= 2(0 + 1 + \cdots + 99) = 2(4950) = 9900\).- Safe. (b) Not safe: loop-carried dependence through
a[i - 1], and no clause fixes it. (c) Not safe as written:bigis shared, butreduction(max: big)fixes it. (d) Not allowed: thebreakmeans OpenMP cannot count the iterations ahead of time.
- Safe. (b) Not safe: loop-carried dependence through
static: thread 0 gets 0–4, and thread 1 gets 5–9.static, 3: thread 0 gets 0, 1, 2, 6, 7, 8, and thread 1 gets 3, 4, 5, 9.dynamic: no. It depends on which thread asks first, and it can change between runs.- The average is \(80/4 = 20\), so \(I = 30/20 = 1.5\), and the best speedup is \(4/1.5 \approx 2.67\).
static, 1ordynamicwith a small chunk might balance better. - Yes. (b) No: the expression uses
x, and it is not one of the allowed forms. (c) Yes. Only the update oftotalis protected.f(i)is computed outside the protection, which is fine, since it does not usetotal.
- Yes. (b) No: the expression uses
- Deadlock: A holds lock 1 and waits for lock 2, while B holds lock 2 and waits for lock 1. Take the locks in the same order in every thread.
- A later phase compares elements that the previous phase may have swapped, so all threads must finish one phase before any start the next. Every thread runs all
npasses of the outer loop. Only the inner loops’ iterations are divided. - A speedup of \(12.0/4.0 = 3.0\). Check that both runs did the same work and produced the same answer, that both were compiled with
-O2, and that the runs were repeated on the same, otherwise idle node.
Outside the exam
You do not need the book’s message-passing program with its queues, OpenMP tasks, caches and false sharing (our class on Tuesday, November 17), OMP_WAIT_POLICY, exact guided chunk sizes, or Pthreads read-write locks and semaphores. MPI is not the focus of this exam, though comparisons between processes and threads are fair game.