NOTE: this website is out of date. This is the course web site from a past quarter, Fall 2023. If you are a current student taking the course, you should visit the current class web site instead. If the current website is not yet visible by going to cs111.stanford.edu, it may be accessible by visiting this link until the new page is mounted at this address. Please be advised that courses' policies change with each new quarter and instructor, and any information on this out-of-date page may not apply to you.
Due: Mon Nov 13 11:59 pm
Late submissions accepted until Wed Nov 15 11:59 pm
Assignment by David Mazières and John Ousterhout, with modifications by Nick Troccoli, Benjamin Xie and Xiyu Zhang. Ethics case study includes material from Kathleen Creel.
In this project you will reflect on the impact of complexities in writing multithreaded code and evaluating trust, and implement code using the "monitor" pattern to complete two synchronization problems with mutexes and condition variables. For the programs you will write, one is for passengers boarding Caltrain, and another is for grouping together guests at a party, and your task is to implement a class for each with the necessary instance variables and code to make the threads coordinate properly.
Learning Goals
- build your skills identifying race conditions and deadlock
- more fully understand where to use mutexes and condition variables as appropriate to coordinate between threads and avoid deadlock/race conditions
- get practice using the "monitor" pattern for designing multithreaded programs
- explore understandings of trust and how we trust the systems we use
Starter Code
To get started on the assignment, clone the starter project using the command
git clone /afs/ir/class/cs111/repos/assign4/$USER assign4
This line creates a new folder called assign4
in your current working directory containing a copy of the starter code.
Here are the relevant files for your work on the assignment:
questions.txt
: a text file where you will add answers to some written questionscaltrain.hh/cc
: the implementation of theStation
class that represents a single train station. You will edit the header file to add necessary instance variables, and edit the cc file to implement the provided methods.caltrain_test.cc
: a test file containing various tests that spawn threads and call theStation
methods in different ways.party.hh/cc
: the implementation of theParty
class that represents a single gathering. You will edit the header file to add necessary instance variables, and edit the cc file to implement the provided methods.party_test.cc
: a test file containing various tests that spawn threads and call theParty
methods in different ways.samples
: a symbolic link to the shared directory for this assignment. It contains:caltrain_test_soln
: a provided samplecaltrain_test
solution executable that uses a fully-implementedStation
class.party_test_soln
: a provided sampleparty_test
solution executable that uses a fully-implementedParty
class.
Writing and Debugging Synchronization Code
The first two parts of the assignment have you write two monitor-style classes. You do not have to write code to create threads; we provide a test harness for each problem that creates instances of your monitor class, plus threads, and then invokes the methods that you will write. Synchronization code is hard to get right, so it's important for it to be clean, simple, and obvious. The best way to get started on synchronization problems like these is to think about what information about the state of the world needs to be kept in a Station
or Party
object. Consider what information is needed to implement the methods. For example, for the Caltrain exercise below:
- What information is needed in order to decide whether a passenger can safely board a train?
- What information is needed to decide when a train can leave the station?
We call these variables state variables because they keep track of the state of the world, such as how many passengers are currently waiting. Once you have an initial guess about the state to maintain, you can then try writing the methods. As you write and test the code, you may discover that your state variables don't have enough information to make important decisions; when that happens, you'll need to add more state variables.
Debugging Irreproducible Failures
Bugs in synchronization code often result in failures that only occur occasionally; most of you will experience problems like this at some point during this assignment. Here are some tips:
-
Stalling / deadlock? Try running the test case in GDB and ctl-c when it stalls, to poke around and see where it's stalling. That may point you to a condition variable wait that is never woken up, or some other issue that may give you more info.
-
Need to gather more information? Add statements to print out information each time a lock is acquired, before and after each condition variable wait, and after any other major decisions are made. Print out lots of information in each of these statements, including where in the code you are, which thread is running (if you can tell), and key state variables. Then pick a test that occasionally fails and run it repeatedly until it does fail (pipe the output to a file). If you walk through the log file, you will probably find a sequence of events that you hadn't expected to occur, and which your code can't handle properly.
-
Trying to replicate an occasional issue? Try adding
sleep_for
calls in your code to "force" a specific ordering of events that exposes an issue, and then debug further from there.
Exercise 1: Caltrain Automation
Caltrain has decided to improve its efficiency by automating not just its trains but also its passengers. From now on, passengers will be robots. Each robot and each train is controlled by a thread. You have been hired to implement synchronization that will guarantee orderly loading of trains. You must implement a Station
class that provides three methods:
load_train(int available)
: when a train arrives in the station and has opened its doors, it invokes this method, whereavailable
indicates how many seats are available on the train. The method must not return until the train is satisfactorily loaded (all passengers are in their seats, and either the train is full or all waiting passengers have boarded).wait_for_train()
: When a passenger robot arrives in a station, it first invokes this method. This method must not return until a train is in the station (i.e., a call toload_train
is in progress) and there are enough free seats on the train for this passenger to sit down. Once this method returns, the passenger robot will begin moving the passenger on board the train and into a seat (you do not need to worry about how this mechanism works).boarded()
: Once a passenger is seated, it will call this method to let the station know that it's safely on board. This method is needed because it takes time for passengers to board the train oncewait_for_train
has given them the go-ahead, and the train mustn't leave the station until all of the admitted passengers are safely in their seats.
Another way to think about the simulation is a passenger can be in one of three states: waiting, boarding, or boarded. A waiting passenger is one who is waiting to board a train - either a train isn't in the station, or it is but there isn't space on the train. A boarding passenger is one who has reserved a seat on the train but is still getting on the train. A boarded passenger is one who has successfully gotten on the train.
Here is some additional information for this problem:
- At any given time, there may exist any number of
Station
objects, passenger threads, and train threads (subject to the restrictions below). - Your code should not invoke any of the above methods (they will be invoked by the test harness).
- You may assume that there is never more than one train in a given
Station
at once, and that all trains (and all passengers) are going to the same destination. Any passenger can board any train. - Your code must allow multiple passengers to board simultaneously (it
must be possible for several passengers to have called
wait_for_train
, and for that method to have returned for each of the passengers, before any of the passengers callsboarded
). - It must be possible to have multiple
Station
objects operating independently (activities in oneStation
should not affect any otherStation
). This means that you shouldn't have anystatic
class variables inStation
(if you don't know what those are, no problem!). - Our solution for each method is < 10 lines long each (excluding comments); Your methods don't necessarily have to be this short, but if they are significantly longer then you're probably taking a more complicated approach than you need.
Recommended Steps
- Write code to maintain the initial set of state variables you decided on; add comments marking the places where the code may need to block or wakeup other threads.
- Next, ignore all of the state variables and add just enough code for an
arriving passenger to block in
wait_for_train
until a train has arrived. When a train arrives, all blocked calls towait_for_train
should return, andload_train
should return immediately. Ignore train capacity, the boarding process and theboarded
method. This isn't correct, of course, but it should allow your code to pass sanity check tests #2 and #3. - Now write enough code for
load_train
to block correctly: as long as passengers are boarding or waiting to board, the train should stay in the station, leaving only when all boarding passengers have invokedboarded
. Continue to ignore train capacity: as long as a train is in the station, passengers can begin boarding. This should pass sanity check tests #4 - #7. - Now factor in train capacity; even if passengers are waiting, a train should leave if it has no more seats. Once a train leaves the station, even if it had open seats, no passengers should board until another train with space arrives. And even if a train is in the station, a passenger shouldn't board if there are no more seats. You should now pass sanity check tests #8 - #9; woohoo!
Testing
When you're debugging you'll find it useful to run tests individually.
The output of sanitycheck
shows the command to run for each test;
for example,
./caltrain_test no_waiting_passengers
will run the first test, in which trains arrive when there are no
passengers waiting. Try invoking this command: it prints out a series of
messages indicating what the test is doing and any errors it encounters.
You can compare this to the output of the sample solution in
samples/caltrain_test_soln
.
When you're debugging, you'll also find it useful to look at the code
in the test harness so you can see exactly what is going on.
For example, the code for the no_waiting_passengers
test is in the method no_waiting_passengers
in caltrain_test.cc
;
each test is implemented by a method with the same name as the test.
Once you have passed all of the sanity checks for this exercise, you
should also test your Caltrain solution by invoking the following
command:
./caltrain_test randomized 1000 50
This will run a randomized test where 1000 passengers arrive at the
station, followed by a series of trains with different capacities
(there will be at most 50 free seats in any one train).
Check the output manually to make sure things look correct.
Try running this test many times to make sure it always
works (see Debugging Irreproducible Failures).
Try varying the parameters as well.
Note: the random test creates a large number of threads; depending on other activity on the machine, it's possible that the system may run out of threads. If this happens, the test will fail with an error message that says "Resource temporarily unavailable". When this happens, try again. If it fails repeatedly, you can either reduce the number of passengers or log out and log in to a different myth machine.
We do not guarantee that the tests we have provided are exhaustive, and may use further autograder tests/review to evaluate correctness.
Here is information about what sequence of events each test is evaluating:
no_waiting_passengers
No passengers are spawned.
- A train with 0 seats arrives (should immediately depart)
- A train with 10 seats arrives (should immediately depart)
passenger_wait_for_train
- A passenger arrives, and begins waiting for a train
- A train with 3 seats arrives
- The passenger should then begin boarding
train_wait_until_boarded
- A passenger arrives, and begins waiting for a train
- A train with 3 seats arrives
- The passenger should then begin boarding
- The train should only depart once the passenger is boarded
passenger_arrives_during_boarding
- A passenger arrives, and begins waiting for a train
- A train with 3 seats arrives
- The passenger should then begin boarding
- While passenger 1 is boarding, another passenger arrives, and should begin boarding
- The first passenger finishes boarding, but the train should not depart yet
- The second passenger finishes boarding, and the train should then depart
board_in_parallel_all
- 4 passengers arrive, and begin waiting for a train
- A train with 4 seats arrives
- All 4 passengers should begin boarding in parallel
- 3 of the passengers finish boarding, but the train should not depart yet
- The last passenger finishes boarding, and the train should then depart
leftover
- A train with 10 seats arrives (should immediately depart)
- A passenger arrives, and begins waiting for a train
- A train with 1 seat arrives
- The passenger should then begin boarding
- The train should only depart once the passenger is boarded
full_train_departs
- A passenger arrives, and begins waiting for a train
- A train with 0 seats arrives (should immediately depart, and passenger should continue waiting)
- A train with 3 seats arrives
- The passenger should then begin boarding
- The train should only depart once the passenger is boarded
board_in_parallel
- 4 passengers arrive, and begin waiting for a train
- A train with 3 seats arrives
- 3 of the 4 passengers should begin boarding in parallel
- The first two passengers finish boarding, but the train should not depart yet
- The 3rd passenger finishes boarding, and the train should then depart with the fourth passenger still waiting
- The last passenger finishes boarding, and the train should then depart
randomized
- A larger number of passengers arrive
- A series of trains arrive one at a time with varying numbers of available seats. They should leave only when they are either full or no more passengers are waiting/boarding.
Exercise 2: Party Introductions
You have been hired as an organizer for parties where people want to be introduced to other guests with certain Zodiac signs. In particular, you must create a C++ class Party
with a single method that can be used to introduce incoming guests to each other for conversation. When a guest arrives at a party, they will invoke the following method on a Party
object:
std::string meet(std::string &my_name, int my_sign, int other_sign);
The my_name
argument gives the name for this guest and my_sign
indicates the guest's Zodiac sign (a number from 0-11). The other_sign
argument indicates that this guest would like to talk to someone with that Zodiac sign. The meet
method must not return until a guest has arrived with matching my_sign
and other_sign
, and meet
must return the name of the matching guest.
In addition:
- Guest names could be any string, including the empty string
- Guests must be matched: if A receive's B's name, then B must receive A's name.
- If a suitable match is not immediately available, then
meet
must wait until a match becomes available. In the meantime, it must be possible for non-interfering matches to occur. - It is possible for multiple guests to have the same name.
- It must be possible to have multiple
Party
objects all operating independently (activities in oneParty
should not affect any otherParty
). This means that you shouldn't have anystatic
class variables inParty
(if you don't know what those are, no problem!). - Guests must be matched fairly: if there are several waiting guests that match a new arrival, the one that has been waiting longest should generally be chosen. See the section below about FIFO behavior for locks and condition variables.
- Our solution for the
meet
method is < 20 lines long (excluding comments); this isn't a hard requirement for you to adhere to, but if your implementation is significantly longer, then you're likely taking a more complicated approach than you need.
Design
For this exercise, use your learnings about design, incremental development and testing from the Caltrain exercise to devise a plan for how to tackle this problem. Here are a few tips:
- Consider what custom structs/data structures may be helpful to store state: C++ has built-in data structures that may come in handy. You can also design your own custom structs to put in them. This is a more open-ended design problem; it's up to you to devise how to best store any necessary state!
- Condition variables can't be copied: you may run into issues when trying to store condition variables in data structures because some data structure operations may require making a copy of an element, which is not allowed with condition variables. Pointers are just the fix for this - they allow us to have references to values without making copies. Consider how pointers can come in handy!
- You should not need to use any heap allocation (e.g.
new
,delete
).
Testing
Here is information about what sequence of events each provided test is evaluating:
two_guests_perfect_match
- guest_a with sign 0 arrives, looking for guest with sign 5 (must wait)
- guest_b with sign 5 arrives, looking for guest with sign 0 (should match with guest_a)
return_in_order
- guest_a with sign 1 arrives, looking for guest with sign 3 (must wait)
- guest_b with sign 1 arrives, looking for guest with sign 3 (must wait)
- guest_c with sign 1 arrives, looking for guest with sign 3 (must wait)
- guest_d with sign 3 arrives, looking for guest with sign 1 (should match with guest_a)
- guest_e with sign 3 arrives, looking for guest with sign 1 (should match with guest_b)
- guest_f with sign 3 arrives, looking for guest with sign 1 (should match with guest_c)
sign_matching
- guest_a with sign 1 arrives, looking for guest with sign 3 (must wait)
- guest_b with sign 2 arrives, looking for guest with sign 1 (must wait)
- guest_c with sign 3 arrives, looking for guest with sign 2 (must wait)
- guest_d with sign 3 arrives, looking for guest with sign 1 (should match with guest_a)
- guest_e with sign 2 arrives, looking for guest with sign 3 (should match with guest_c)
- guest_f with sign 1 arrives, looking for guest with sign 2 (should match with guest_b)
single_sign
- guest_a with sign 2 arrives, looking for guest with sign 2 (must wait)
- guest_b with sign 2 arrives, looking for guest with sign 2 (should match with guest_a)
single_sign_many
- 10 guests arrive in total, all with the same sign, and pairs of guests should match (e.g. guest 1 should match with guest 2, guest 3 with guest 4, etc.).
same_name
- Guest (that's their name) with sign 4 arrives, looking for guest with sign 5 (must wait)
- Guest (that's their name) with sign 4 arrives, looking for guest with sign 5 (must wait)
- Guest (that's their name) with sign 5 arrives, looking for guest with sign 4 (should match with first Guest)
- Guest (that's their name) with sign 5 arrives, looking for guest with sign 4 (should match with second Guest)
random_party
- A larger number of guests arrive, such that every guest can eventually find a match.
There is also the random_party
test that you can run many times with various parameters. You can run it like this:
./party_test random_party 100 4
This runs a randomized test with a large number of party guests
with different preferences. The first parameter (100) indicates how
many guests will arrive, and the second parameter indicates how many
distinct signs should appear among all the arrivals (4 means that
only 4 of the numbers between 1 and 12 will be used as my_sign
or
other_sign
; a smaller value like 4 is more likely to produce
conflicts that expose bugs). Check the output manually to make sure
there are no error messages. Try running this test many times (and
also try varying the parameters), in
order to generate a variety of scenarios.
Are Locks and Condition Variables FIFO?
The C++ classes for locks and condition variables are mostly FIFO (First In First Out: the first thread to wait is the first thread to wakeup) but not perfectly so. If you're curious to see this, try running
./party_test cond_fifo
This runs a test to see if condition variables are perfectly FIFO. If
you run it several times, you'll see that it sometimes fails (the first
thread to wait on a condition variable doesn't necessarily get notified
first).
For your assignments, you can assume that this behavior is close enough
to FIFO for fairness considerations (e.g. this is good enough for the
"fair matching" requirement for Party
).
However, your code must not depend on perfect FIFO behavior for correctness: for example, it is not safe to assume that when you notify a condition variable, the first thread on the queue is guaranteed to be the first thread to acquire the lock. Even if condition variables were perfectly FIFO, if two threads are notified at about the same time, the scheduler might choose to run them in any order, and this will affect which one gets the lock first.
Exercise 3: Reflecting on and Communicating Trust
As you may know, the Computer Science Department has begun adding short units on ethics to many of its classes. For CS 111 the ethics topic is trust. The overall goal of this topic is to raise your awareness of a variety of issues relating to trust. For this assignment you will consider techniques for establishing trust in a system.
Building on the lecture discussion about trust, answer the following questions in questions.txt
:
Question 1: suppose you needed to convince another student in CS 111 that they can trust your code for this assignment. How would you go about that? Note: for someone else to trust your code, you may want to consider how you can increase the legitimacy of either the code itself or the creator of the code.
Question 2: for this assignment, the provided sanitycheck tests are not necessarily comprehensive. What additional steps could/did you take to increase your level of trust that the code will perform as expected?
Software bugs are examples of situations where systems are less
trustworthy than we think or would like.
They are to difficult to test for, yet
they can have significant real-world impact. In 2020, Natalie Silvanovich
of Google Project Zero discovered a race condition with Google’s video
calling service, Duo. Please read this document,
which describes the problem, and then apply Nguyen’s framework of trust as an
unquestioning attitude to a real world race condition by answering
the questions below in questions.txt
:
Question 3: describe the race condition and its impact.
Question 4: in 1-3 sentences, describe one way developers of the Google Duo bug exhibit agential gullibility.
Question 5: in 1-3 sentences, identify two ways a mobile phone user could infer trust in Google Duo.
Question 6: in 1-3 sentences, identify one way a mobile phone user could substitute the need to trust Google Duo.
Other Coding Requirements
- Your solutions for both problems should be written in C++ and use the C++ library classes for synchronization.
- There must be no busy-waiting in your solutions.
- You must use the monitor design pattern for synchronization, as discussed in lecture. In particular, you may not declare any further
mutex
es other than the ones declared in the starter code.
Submitting and Grading
Once you are finished working and have saved all your changes, submit by running the submit
tool in the tools
folder: ./tools/submit
. We recommend you do a trial submit in advance of the deadline to allow time to work through any snags. You may submit as many times as you would like; we will grade the latest submission. Submitting a stable but unpolished/unfinished is like an insurance policy. If the unexpected happens and you miss the deadline to submit your final version, this previous submit will earn points. Without a submission, we cannot grade your work. You can confirm the timestamp of your latest submission in your course gradebook.
Your assignment will be graded using the tests we expose via sanitycheck, plus the following:
- potential additional autograder tests to test correctness
- reviewing your
questions.txt
responses for the short answer questions - ensuring your program has no busy waiting or forced sleeping (i.e. calls to
usleep
) - ensuring your program has no race conditions or other concurrency issues
- no static class variables or other designs that mean instances of
Station
orParty
are interdependent - ensuring compliance with the monitor pattern, meaning
Station
has exactly 1 mutex, andParty
has exactly 1 mutex - Style code review - Synchronization code is hard to get right, so it's important for it to be clean, simple, and obvious. Your goal should be simplicity, not just line count; simple programs are usually shorter than complex ones, but the shortest program isn't always the simplest. Check out our style guide for other style tips.