Principles of Computer Systems
Winter 2021
Stanford University
Computer Science Department
Instructors: Chris Gregg and
Nick Troccoli
Introduction to Threads
Threads and Mutexes
Condition Variables and Semaphores
Multithreading Patterns
Multithreading Examples
Layered Construction (combo) - combine multiple patterns
Examples:
static mutex rgenLock;
static RandomGenerator rgen;
static unsigned int getNumCones() {
lock_guard<mutex> rgenLockGuard(rgenLock);
return rgen.getNextInt(kMinConeOrder, kMaxConeOrder);
}
static unsigned int getBrowseTimeMS() {
lock_guard<mutex> rgenLockGuard(rgenLock);
return rgen.getNextInt(kMinBrowseTimeMS, kMaxBrowseTimeMS);
}
static unsigned int getPrepTimeMS() {
lock_guard<mutex> rgenLockGuard(rgenLock);
return rgen.getNextInt(kMinPrepTimeMS, kMaxPrepTimeMS);
}
static unsigned int getInspectionTimeMS() {
lock_guard<mutex> rgenLockGuard(rgenLock);
return rgen.getNextInt(kMinInspectionTimeMS, kMaxInspectionTimeMS);
}
static bool getInspectionOutcome() {
lock_guard<mutex> rgenLockGuard(rgenLock);
return rgen.getNextBool(kConeApprovalProbability);
}
To model a "real" ice cream store, we want to randomize different occurrences throughout the program. We use these functions to do that.
To model a "real" ice cream store, we want to randomize different occurrences throughout the program. We use these functions to do that.
static void browse() {
cout << oslock << "Customer starts to kill time." << endl << osunlock;
unsigned int browseTimeMS = getBrowseTimeMS();
sleep_for(browseTimeMS);
cout << oslock << "Customer just killed " << double(browseTimeMS) / 1000
<< " seconds." << endl << osunlock;
}
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 prepTimeMS = getPrepTimeMS();
sleep_for(prepTimeMS);
cout << oslock << " Clerk just spent " << double(prepTimeMS) / 1000
<< " seconds making ice cream cone #" << coneID
<< " for customer #" << customerID << "." << endl << osunlock;
}
int main(int argc, const char *argv[]) {
// Make an array of customer threads, and add up how many cones they order
int totalConesOrdered = 0;
thread customers[kNumCustomers];
for (size_t i = 0; i < kNumCustomers; i++) {
int numConesWanted = getNumCones();
customers[i] = thread(customer, i, numConesWanted);
totalConesOrdered += numConesWanted;
}
/* Make the manager and cashier threads to approve cones / checkout customers.
* Tell the manager how many cones will be ordered in total. */
thread managerThread(manager, totalConesOrdered);
thread cashierThread(cashier);
// Join all the threads
for (thread& customer: customers) customer.join();
cashierThread.join();
managerThread.join();
return 0;
}
In main, we spawn all of the customers, the manager (telling it the total number of cones ordered), and the cashier. Why not clerks? Each customer spawns its own clerks.
Then, we wait for the threads to finish.
A customer does the following:
"gets its number in checkout line" - global counter, needs a binary lock
"tells cashier we are ready to check out" - one generalized coordination semaphore
"waits for cashier to ring us up" - binary coordination semaphore per customer
struct checkout {
checkout(): nextPlaceInLine(0) {}
atomic<unsigned int> nextPlaceInLine;
semaphore waitingCustomers;
semaphore customers[kNumCustomers];
} checkout;
Global struct shared by all customers and the cashier.
static void customer(unsigned int id, unsigned int numConesWanted) {
// Make a vector of clerk threads, one per cone to be ordered
vector<thread> clerks(numConesWanted);
for (unsigned int i = 0; i < clerks.size(); i++) {
clerks[i] = thread(clerk, i, id);
}
browse();
for (thread& clerk: clerks) clerk.join();
// Now we are ready to check out. Get our unique place in line.
int place = checkout.nextPlaceInLine++;
cout << oslock << "Customer " << id << " assumes position #" << place
<< " at the checkout counter." << endl << osunlock;
checkout.waitingCustomers.signal();
checkout.customers[place].wait();
cout << oslock << "Customer " << id
<< " has checked out and leaves the ice cream store."
<< endl << osunlock;
}
A customer does the following:
1) spawns a clerk for each cone
2) browses and waits for clerks
3) gets its place in checkout line
4) tells cashier it's there
5) waits for cashier to ring it up
struct checkout {
checkout(): nextPlaceInLine(0) {}
atomic<unsigned int> nextPlaceInLine;
semaphore waitingCustomers;
semaphore customers[kNumCustomers];
} checkout;
A clerk does the following:
"attempts to get exclusive access to the manager" - binary lock
"tells the manager it needs approval" - binary coordination semaphore
"waits for the manager to decide..." - binary coordination semaphore
struct {
mutex available;
semaphore requested;
semaphore finished;
bool passed;
} inspection;
Global struct shared by all clerks and the manager.
static void clerk(unsigned int coneID, unsigned int customerID) {
bool success = false;
while (!success) {
makeCone(coneID, customerID);
// We must be the only one requesting approval
inspection.available.lock();
// Let the manager know we are requesting approval
inspection.requested.signal();
// Wait for the manager to finish
inspection.finished.wait();
/* If the manager is finished, it has put its approval
* decision into "passed" */
success = inspection.passed;
// We're done requesting approval
inspection.available.unlock();
}
}
A clerk does the following:
struct {
mutex available;
semaphore requested;
semaphore finished;
bool passed;
} inspection;
"waits for a clerk's cone to inspect" - binary coordination semaphore
"tells the clerk that we are done" - binary coordination semaphore
The single manager does the following while there are more cones needed:
struct {
mutex available;
semaphore requested;
semaphore finished;
bool passed;
} inspection;
Global struct shared by all clerks and the manager.
static void manager(unsigned int numConesNeeded) {
unsigned int numConesAttempted = 0;
unsigned int numConesApproved = 0;
while (numConesApproved < numConesNeeded) {
// Wait for someone to request an inspection
inspection.requested.wait();
inspectCone();
// Let them know we have finished inspecting
inspection.finished.signal();
// Update our counters
numConesAttempted++;
if (inspection.passed) numConesApproved++;
}
cout << oslock << " Manager inspected a total of "
<< numConesAttempted
<< " ice cream cones before approving a total of "
<< numConesNeeded
<< "." << endl << " Manager leaves the ice cream store."
<< endl << osunlock;
}
The manager does the following while there are more cones needed:
struct {
mutex available;
semaphore requested;
semaphore finished;
bool passed;
} inspection;
static void inspectCone() {
cout << oslock << " Manager is presented with an ice cream cone."
<< endl << osunlock;
// Sleep for to simulate inspecting the cone
unsigned int inspectionTimeMS = getInspectionTimeMS();
sleep_for(inspectionTimeMS);
// Generate a decision and put it into the `passed` field
// No locks needed - we are only one accessing right now
inspection.passed = getInspectionOutcome();
string verb = inspection.passed ? "APPROVED" : "REJECTED";
cout << oslock << " Manager spent "
<< double(inspectionTimeMS) / 1000
<< " seconds analyzing presented ice cream cone and "
<< verb << " it."
<< endl << osunlock;
}
The manager does the following while there are more cones needed:
struct {
mutex available;
semaphore requested;
semaphore finished;
bool passed;
} inspection;
"waits for a customer to be ready to check out" - generalized coordination semaphore
"tells the i-th customer that it has checked out" - binary coordination semaphore per customer
The single cashier does the following while there are more customers to ring up:
struct checkout {
checkout(): nextPlaceInLine(0) {}
atomic<unsigned int> nextPlaceInLine;
semaphore waitingCustomers;
semaphore customers[kNumCustomers];
} checkout;
Global struct shared by all customers and the cashier.
static void cashier() {
cout << oslock
<< " Cashier is ready to help customers check out."
<< endl << osunlock;
// We check out all customers
for (unsigned int i = 0; i < kNumCustomers; i++) {
// Wait for someone to let us know they are ready to check out
checkout.waitingCustomers.wait();
cout << oslock << " Cashier rings up customer " << i << "."
<< endl << osunlock;
// Let the ith customer know that they can leave.
checkout.customers[i].signal();
}
cout << oslock << " Cashier is all done and can go home."
<< endl << osunlock;
}
The cashier does the following while there are more customers to ring up:
struct checkout {
checkout(): nextPlaceInLine(0) {}
atomic<unsigned int> nextPlaceInLine;
semaphore waitingCustomers;
semaphore customers[kNumCustomers];
} checkout;
Next time: Networking