Ancient CS 61 Content Warning!!!!!1!!!
This is not the current version of the class.
This site was automatically translated from a wiki. The translation may have introduced mistakes (and the content might have been wrong to begin with).

Threads and Concurrency

Thread Overview

A thread is a logical flow of control that runs in the context of a process

The following figure shows the memory organization of a single-threaded and a multi-threaded process. Threadstack.png

Useful POSIX Thread APIs

This link to the pthread library http://pubs.opengroup.org/onlinepubs/009695399/basedefs/pthread.h.html

Creating and Destroying Threads

int pthread_create(
  pthread_t *restrict thread,
  const pthread_attr_t *restrict attr,
  void *(*start_routine)(void *),
  void *restrict arg);
)
void pthread_exit(void* value_ptr)
int pthread_join(pthread_t thread, void** value_ptr)
int pthread_detach(pthread_t pthread)

For more details and examples on how to use these functions, you can access their man pages, e.g. man pthread_create.

Important Note

In order to avoid segmentation faults, when using pthread_create, the arguments are passed as a void\* — a generic pointer! This has two major consequences of which you should be aware:

  1. Anything can be passed in: any pointer can be autocast to a void\*, and there is consequently no typechecking when you pass arguments via pthread_create.
  2. The void\* argument of start_routine can be cast to anything: a void\* can also be cast to any other pointer type without compiler warnings.

This means that you, as the programmer, must explicitly be aware of the types being passed in. If you expect a size_t\* going in, then you must manually ensure that all calls pass in a size_t\*. The compiler will not do this for you. Otherwise, you might cast into the wrong type, and consequently encounter segmentation faults left and right.

Synchronization

Concurrency can cause race conditions, an undesirable side effect. Therefore, we need to synchronize access to shared data across threads within our program. To do so, we must ensure mutual exclusion in critical sections:

Mutual exclusion requires hardware support (e.g. atomic test and set, atomic compare and swap, load-link/store-conditional). Given this basic hardware support, we can build higher level synchronization primitives. We will discuss 5 types of synchronization objects next: mutex, condition variable, spinlock, semaphore, and monitor.

Mutex (Mutual exclusion lock)

Operations on a mutex (we give the general operation first, then the pthread-specific function name):

Condition Variable

Condition variable (CV): a synchronization object that represents a condition (a boolean expression that can be either true or false)

Operations on a CV:

For example, let's understand the following code together:

cv.png

Check out this link for more details: http://cs61.seas.harvard.edu/wiki/2016/ConditionVariables

Spinlock

Semaphore

Monitor

Deadlock

Exercise: Bathroom Synchronization

A series of fun bathroom-themed conceptual exercises for you to get familiar with synchronization primitives!

Stall Privacy

In this exercise, the setting is a bathroom with K stalls numbered 0 through K–1. A number of users enter the bathroom; each has a preferred stall. Each user can be modeled as an independent thread running the following code:

void user(int preferred_stall) {
    poop_into(preferred_stall);
}

You probably can intuitively smell that this bathroom model has a synchronization problem. Answer the following questions without referring to the solutions first.

Q1. What synchronization property is missing?

Q2. Where is the critical section in user, and when do two users conflict?

Q3. Provide synchronization using a single mutex, i.e., using coarse-grained locking. You can use pthread syntax for this mutex, or make up your own syntax (as long as the meaning is clear). Your solution should be correct (it should avoid gross conditions [which are race conditions in the context of a bathroom-themed synchronization problem]), but it need not be efficient. Your solution will consist of (1) a definition for the mutex, which must be a global variable, and (2) new code for user().

Q4. Provide synchronization using fine-grained locking. Your solution should be more efficient than the previous solution. Specifically, it should allow the bathroom to achieve full utilization: it should be possible for all K stalls to be in use at the same time. You will need to change both the global mutex — there will be more than one global mutex — and the code for user().

Stall choice

We use the same setting as above, except that users have no preferred stall:

void user(void) {
    int preferred_stall = random() % K;
    poop_into(preferred_stall);
}

Q5. Adapt this function to use your synchronization solution from Q4.

Q6. As written, user picks a preferred stall and then waits for that stall to become available. This isn’t as efficient as possible, since multiple users might pick the same stall by chance. Write a synchronization solution where each user chooses any available stall. First, use a single mutex coupled with a global data structure representing stall occupancy.

Any correct solution will contain at least two different calls to lock (or trylock) functions and two different calls to unlock functions. A solution with only one lock call has either a race condition, a deadlock, or underutilization (at most one user in the bathroom at a time).

Q7. Second, repeat your solution using multiple mutexes and no additional global variables.

Q8. Compare these solutions in terms of the number of synchronization calls made per user.

Bathroom capacity

Your solution in Q8 allows an arbitrary number of users to wait in the bathroom for a stall. The fire marshal says no no: at most K + 4 occupants are allowed at a time (one per stall, plus 4 using the sink). We want a user model like this:

void user(void) {
    while (1) {
        if (the bathroom has fewer than K+4 occupants) {
            enter bathroom;
            poop in empty stall;
            leave bathroom;
            return;
        }
    }
}

Q9. Which parts of this user model must be executed atomically to avoid race conditions where the bathroom has more than K+4 occupants? That is, which critical section avoids overoccupancy?

Q10. Write a synchronization solution that avoids both overoccupancy and gross (race) conditions. Use only one or more mutexes — no condition variables. Tell us what global state you use.

As synchronization problems get more complicated, more code is required to solve them. You may want to sketch pseudocode or copy and paste working code from earlier exercises.

Q11. Is your solution blocking or polling?

Q12. Improve your solution’s CPU usage by adding one or more condition variables. In your solution, a user thread should block until there’s a very good chance that the bathroom has fewer than K+4 occupants. Specify what predicate is associated with each condition variable.

Plumbing protection

Q13. Harvard has old plumbing, and if more than 3 users flush at the same time, the bathroom will explode. Write code that avoids this gross condition, starting from your solution to Q12. Your user threads should block until they have a chance to flush, and it should be possible for 3 users to flush simultaneously. This will involve another condition variable (why?).

void user(void) {
    while (1) {
        if (the bathroom has fewer than K+4 occupants) {
            enter bathroom;
            poop in empty stall;
            while (3 or more users are flushing) {
                block; // heh
            }
            flush;
            leave bathroom;
            return;
        }
    }
}

Poopers and cleaners

Bathrooms sometimes need to be cleaned. In this version of the synchronization problem, there are two kinds of users, poopers and cleaners. A bathroom may have one or more poopers in it, or one or more cleaners, but it should never have both a pooper and a cleaner. The threads can be modeled this way:

void pooper(void) {
    while (1) {
        if (the bathroom has no cleaners) {
            enter bathroom;
            poop;
            leave bathroom;
            return;
        }
    }
}
void cleaner(void) {
    while (1) {
        if (the bathroom has no poopers) {
            enter bathroom;
            clean;
            leave bathroom;
            return;
        }
    }
}

For simplicity’s sake, we’ve dropped most of the earlier parts (stall synchronization, bathroom capacity, and plumbing protection).

Q14. Write a solution for this problem that involves mutexes only. As usual, this solution will cause some users to poll.

Q15. Write a solution for this problem involving one condition variable. A user should block until they have some chance to enter the bathroom. However, since you have only one condition variable, your solution will necessarily involve false wakeups (you may wake up a user who cannot enter the bathroom yet).

Q16. Many solutions for Q15 should involve the broadcast operation, and would go wrong if they used signal. Does your solution have this property? Explain.

Q17. Write a solution for this problem involving two condition variables.

Pooping philosophers

To facilitate the communal philosophical life, Plato decreed that philosophy should be performed in a bathroom with K thrones and K curtains, arranged like so:

File:P1.png

Each philosopher sits on a throne with two curtains, one on each side. The philosophers normally argue with one another. But sometimes a philosopher’s got to poop, and to poop one needs privacy. A pooping philosopher must first draw the curtains on either side; philosopher X will need curtains X and (X+1) mod K. For instance, here, philosophers 0 and 2 can poop simultaneously, but philosopher 4 cannot poop until philosopher 0 is done (because philosopher 4 needs curtain 0), and philosopher 1 cannot poop until philosophers 0 and 2 are both done:

File:P2.png

Q18. In high-level terms, what is the mutual exclusion problem here—that is, what resources are subject to mutual exclusion?

Q19. A philosopher could be described like this:

void philosopher(int x) {
    while (1) {
        argue();
        draw curtains x and (x + 1) % K;
        poop();
        release curtains x and (x + 1) % K;
    }
}

Write a correctly-synchronized version of this philosopher. Your solution should not suffer from deadlock, and philosophers should block (not poll) when they are waiting to poop.

We strongly suggest you attempt each question before checking the answers!

Solutions: https://cs61.seas.harvard.edu/wiki/2017/Section12Solutions