Principles of Computer Systems
Spring 2019
Stanford University
Computer Science Department
Lecturer: Chris Gregg
thread
s can communicate with each other through the use of mutex
es and semaphores (which, as you will recall, is based on a conditional_variable_any).The generalized rendezvous is a combination of binary rendezvous and generalized counter, where a single semaphore is used to record how many times something has occurred.
For example, if thread A spawned 5 thread Bs and needs to wait for all of them make a certain amount of progress before advancing, a generalized rendezvous might be used.
The generalized rendezvous is initialized to 0. When A needs to sync up with the others, it will call wait on the semaphore in a loop, one time for each thread it is syncing up with. A doesn't care which specific thread of the group has finished, just that another has. If A gets to the rendezvous point before the threads have finished, it will block, waking to "count" each child as it signals and eventually move on when all dependent threads have checked back.
If all the B threads finish before A arrives at the rendezvous point, it will quickly decrement the multiply-incremented semaphore, once for each thread, and move on without blocking.
The current value of the generalized rendezvous semaphore gives you a count of the number of tasks that have completed that haven't yet been checked, and it will be somewhere between 0 and N at all times.
The generalized rendezvous pattern is most often used to regroup after some divided task, such as waiting for several network requests to complete, or blocking until all pages in a print job have been printed.
As with the generalized counter, it’s occasionally possible to use thread::join instead of semaphore::wait, but that requires the child threads fully exit before the joining parent is notified, and that’s not always what you want (though if it is, then join is just fine).
The program we will create simulates the daily activity in an ice cream store.
The simulation’s actors are the clerks who make ice cream cones, the single manager who supervises, the customers who buy ice cream cones, and the single cashier who accepts payment from customers. A different thread is launched for each of these actors.
Each customer orders a few ice cream cones, waits for them to be made, gets in line to pay, and then leaves.
customers are in a big hurry and don’t want to wait for one clerk to make several cones, so each customer dispatches one clerk thread for each ice cream cone he/she orders.
Once the customer has all ordered ice cream cones, he/she gets in line at the cashier and waits his/her turn. After paying, each customer leaves.
Each clerk thread makes exactly one ice cream cone. The clerk scoops up a cone and then has the manager take a look to make sure it is absolutely perfect. If the cone doesn't pass muster, it is thrown away and the clerk makes another. Once an ice cream cone is approved, the clerk hands the gem of an ice cream cone to the customer and is then done.
The single manager sits idle until a clerk needs his or her freshly scooped ice cream cone inspected. When the manager hears of a request for an inspection, he/she determines if it passes and lets the clerk know how the cone fared. The manager is done when all cones have been approved.
The customer checkout line must be maintained in FIFO order. After getting their cones, a customer "takes a number" to mark their place in the cashier queue. The cashier always processes customers from the queue in order.
The cashier naps while there are no customers in line. When a customer is ready to pay, the cashier handles the bill. Once the bill is paid, the customer can leave. The cashier should handle the customers according to number. Once all customers have paid, the cashier is finished and leaves.
Let's look at the following in turn:
random time/cone-perfection generation functions
struct inspection
struct checkout
customer
browse
clerk
makeCone
manager
inspectCone
cashier
main
static mutex rgenLock;
static RandomGenerator rgen;
static unsigned int getNumCones() {
lock_guard<mutex> lg(rgenLock);
return rgen.getNextInt(kMinConeOrder, kMaxConeOrder);
}
static unsigned int getBrowseTime() {
lock_guard<mutex> lg(rgenLock);
return rgen.getNextInt(kMinBrowseTime, kMaxBrowseTime);
}
static unsigned int getPrepTime() {
lock_guard<mutex> lg(rgenLock);
return rgen.getNextInt(kMinPrepTime, kMaxPrepTime);
}
static unsigned int getInspectionTime() {
lock_guard<mutex> lg(rgenLock);
return rgen.getNextInt(kMinInspectionTime, kMaxInspectionTime);
}
static bool getInspectionOutcome() {
lock_guard<mutex> lg(rgenLock);
return rgen.getNextBool(kConeApprovalProbability);
}
main
and then pass them into each thread and function by reference or pointer, but this simplifies it (though it does pollute the global namespace).struct inspection {
mutex available;
semaphore requested;
semaphore finished;
bool passed;
} inspection;
struct checkout {
checkout(): nextPlaceInLine(0) {}
atomic<unsigned int> nextPlaceInLine;
semaphore customers[kNumCustomers];
semaphore waitingCustomers;
} checkout;
Customers in our ice cream store, order cones, browse while waiting for them to be made, then wait in line to pay, and then leave. The customer function handles all of the details of the customer's ice cream store visit:
static void customer(unsigned int id, unsigned int numConesWanted) {
// order phase
vector<thread> clerks;
for (unsigned int i = 0; i < numConesWanted; i++)
clerks.push_back(thread(clerk, i, id));
browse();
for (thread& t: clerks) t.join();
// checkout phase
int place;
cout << oslock << "Customer " << id << " assumes position #"
<< (place = checkout.nextPlaceInLine++) << " at the checkout counter."
<< endl << osunlock;
checkout.waitingCustomers.signal();
checkout.customers[place].wait();
cout << "Customer " << id << " has checked out and leaves the ice cream store."
<< endl << osunlock;
}
The customer needs one clerk for each cone. The customer browses and then must join all of the threads before checking out.
The customers line up by signaling for checkout.
The customers wait in line until they are checked out.
Note that the customer starts a clerk thread, and clerks are not waiting around like the manager or cashier.
The browse function is straightforward:
static void browse() {
cout << oslock << "Customer starts to kill time." << endl << osunlock;
unsigned int browseTime = getBrowseTime();
sleep_for(browseTime);
cout << oslock << "Customer just killed " << double(browseTime)/1000
<< " seconds." << endl << osunlock;
}
The sleep_for function pushes the thread off the processor, so it is not busy-waiting.
A clerk has multiple duties: make a cone, then pass it to a manager and wait for it to be inspected, then check to see if the inspection passed, and if not, make another and repeat until a well-made cone passes inspection:
static void clerk(unsigned int coneID, unsigned int customerID) {
bool success = false;
while (!success) {
makeCone(coneID, customerID);
inspection.available.lock();
inspection.requested.signal();
inspection.finished.wait();
success = inspection.passed;
inspection.available.unlock();
}
}
The clerk and the manager use the inspection struct to pass information -- note that there is only a single inspection struct, but that is okay because there is only one manager doing the inspecting.
This does not mean that we can remove the available lock -- it is critical because there are many clerks trying to get the manager's attention.
Note that we only acquire the lock after making the cone -- don't over-lock.
Note also that we signal the manager that we have a cone ready for inspection -- this wakes up the manager if they are sleeping. If the manger is in the middle of an inspection, they will immediately go to the next cone after the inspection.
The makeCone function is straightforward:
static void makeCone(unsigned int coneID, unsigned int customerID) {
cout << oslock << " Clerk starts to make ice cream cone #" << coneID
<< " for customer #" << customerID << "." << endl << osunlock;
unsigned int prepTime = getPrepTime();
sleep_for(prepTime);
cout << oslock << " Clerk just spent " << double(prepTime)/1000
<< " seconds making ice cream cone#" << coneID
<< " for customer #" << customerID << "." << endl << osunlock;
}
The manager (somehow) starts out the day knowing how many cones they will have to approve (we could probably handle this with a global "all done!" flag)
The manager waits around for a clerk to hand them a cone to inspect.For each cone that needs to be approved, the manager inspects the cone, then updates the number of cones approved (locally) if it passes. If it doesn't pass, the manger waits again. When the manager has passed all necessary cones, they go home.
static void manager(unsigned int numConesNeeded) {
unsigned int numConesAttempted = 0; // local variables secret to the manager,
unsigned int numConesApproved = 0; // so no locks are needed
while (numConesApproved < numConesNeeded) {
inspection.requested.wait();
inspectCone();
inspection.finished.signal();
numConesAttempted++;
if (inspection.passed) numConesApproved++;
}
cout << oslock << " Manager inspected a total of " << numConesAttempted
<< " ice cream cones before approving a total of " << numConesNeeded
<< "." << endl;
cout << " Manager leaves the ice cream store." << endl << osunlock;
}
The manager signals the waiting clerk that the cone has been inspected (why can there only be one waiting clerk?)
The inspectCone function updates the inspection struct:
static void inspectCone() {
cout << oslock << " Manager is presented with an ice cream cone."
<< endl << osunlock;
unsigned int inspectionTime = getInspectionTime();
sleep_for(inspectionTime);
inspection.passed = getInspectionOutcome();
const char *verb = inspection.passed ? "APPROVED" : "REJECTED";
cout << oslock << " Manager spent " << double(inspectionTime)/1000
<< " seconds analyzing presented ice cream cone and " << verb << " it."
<< endl << osunlock;
}
Why aren't there any locks needed here? This is a global struct!
The cashier (somehow) knows how many customers there will be during the day. Again, we could probably handle telling the cashier to go home with a global variable.
The cashier first waits for a customer to enter the line, and then signals that particular customer that they have paid.
static void cashier() {
cout << oslock << " Cashier is ready to take customer money."
<< endl << osunlock;
for (unsigned int i = 0; i < kNumCustomers; i++) {
checkout.waitingCustomers.wait();
cout << oslock << " Cashier rings up customer " << i << "."
<< endl << osunlock;
checkout.customers[i].signal();
}
cout << oslock << " Cashier is all done and can go home." << endl;
}
Could we have handled the customer/cashier as we handled the clerks/manager, without the array?
The cashier (somehow) knows how many customers there will be during the day. Again, we could probably handle telling the cashier to go home with a global variable.
The cashier first waits for a customer to enter the line, and then signals that particular customer that they have paid.
static void cashier() {
cout << oslock << " Cashier is ready to take customer money."
<< endl << osunlock;
for (unsigned int i = 0; i < kNumCustomers; i++) {
checkout.waitingCustomers.wait();
cout << oslock << " Cashier rings up customer " << i << "."
<< endl << osunlock;
checkout.customers[i].signal();
}
cout << oslock << " Cashier is all done and can go home." << endl;
}
Could we have handled the customer/cashier as we handled the clerks/manager, without the array?
We must ensure that customers get handled in order (otherwise, chaos) -- not so for the clerks, who can fight for the manager's attention.
Finally, we can look at the main function.
The main function's job is to set up the customers, manager, and cashier. Why not the clerks? (they are set up in the customer function)
int main(int argc, const char *argv[]) {
int totalConesOrdered = 0;
thread customers[kNumCustomers];
for (unsigned int i = 0; i < kNumCustomers; i++) {
int numConesWanted = getNumCones();
customers[i] = thread(customer, i, numConesWanted);
totalConesOrdered += numConesWanted;
}
thread m(manager, totalConesOrdered);
thread c(cashier);
for (thread& customer: customers) customer.join();
c.join();
m.join();
return 0;
}
main must wait for all of the threads it created to join before exiting.
Now we see how the manager and cashier know how many cones / customers there are -- we let everyone in at the beginning, ask them how many cones they want, and off we go.