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
https://commons.wikimedia.org/wiki/File:An_illustration_of_the_dining_philosophers_problem.png
When coding with threads, you need to ensure that:
Goal: we must encode constraints into our program.
Example: how many philosophers can hold a fork at the same time? One.
How can we encode this into our program? Let's make a mutex for each fork.
Goal: we must encode constraints into our program.
Example: how many philosophers can try to eat at the same time? Four.
How can we encode this into our program?
What does this look like in code?
A semaphore is a variable type that represents a count of finite resources.
class semaphore {
public:
semaphore(int value = 0);
void wait();
void signal();
private:
int value;
std::mutex m;
std::condition_variable_any cv;
}
What does a semaphore
initialized with a positive number mean?
semaphore permits(3);
What does a semaphore
initialized with 0 mean?
semaphore permits(0);
void create(int creationCount, semaphore &s) {
for (int i = 0; i < creationCount; i++) {
cout << oslock << "Now creating " << i << endl << osunlock;
s.signal();
}
}
void consume_after_create(int consumeCount, semaphore &s) {
for (int i = 0; i < consumeCount; i++) {
s.wait();
cout << oslock << "Now consuming " << i << endl << osunlock;
}
}
int main(int argc, const char *argv[]) {
semaphore zeroSemaphore(0); // can omit (0), since default initializes to 0
int numIterations = 5;
thread thread_waited_on(create, numIterations, ref(zeroSemaphore));
thread waiting_thread(consume_after_create, numIterations, ref(zeroSemaphore));
thread_waited_on.join();
waiting_thread.join();
return 0;
}
$ ./zeroSemaphore
Now creating 0
Now creating 1
Now creating 2
Now creating 3
Now creating 4
Now consuming 0
Now consuming 1
Now consuming 2
Now consuming 3
Now consuming 4
What does a semaphore
initialized with a negative number mean?
semaphore permits(-9);
The semaphore must reach 1 before the initial wait would end. E.g. you want to wait until other threads finish before a final thread continues.
void writer(int i, semaphore &s) {
cout << oslock << "Sending signal " << i << endl << osunlock;
s.signal();
}
void read_after_ten(semaphore &s) {
s.wait();
cout << oslock << "Got enough signals to continue!" << endl << osunlock;
}
int main(int argc, const char *argv[]) {
semaphore negSemaphore(-9);
thread writers[10];
for (size_t i = 0; i < 10; i++) {
writers[i] = thread(writer, i, ref(negSemaphore));
}
thread r(read_after_ten, ref(negSemaphore));
for (thread &t : writers) t.join();
r.join();
return 0;
}
$ ./negativeSemaphores
Sending signal 0
Sending signal 1
Sending signal 2
Sending signal 3
Sending signal 5
Sending signal 7
Sending signal 8
Sending signal 9
Sending signal 6
Sending signal 4
Got enough signals to continue!
semaphores can be used to support thread coordination.
Let's implement a program that requires thread coordination with semaphores. First, we'll look at a version without semaphores to see why they are necessary.
int main(int argc, const char *argv[]) {
// Create an empty buffer
char buffer[kNumBufferSlots];
memset(buffer, ' ', sizeof(buffer));
thread writer(writeToBuffer, buffer, sizeof(buffer), kNumIterations);
thread reader(readFromBuffer, buffer, sizeof(buffer), kNumIterations);
writer.join();
reader.join();
return 0;
}
static void readFromBuffer(char buffer[], size_t bufferSize, size_t iterations) {
cout << oslock << "Reader: ready to read." << endl << osunlock;
for (size_t i = 0; i < iterations * bufferSize; i++) {
// Read and process the data
char ch = buffer[i % bufferSize];
processData(ch); // sleep to simulate work
buffer[i % bufferSize] = ' ';
cout << oslock << "Reader: consumed data packet "
<< "with character '" << ch << "'.\t\t" << osunlock;
printBuffer(buffer, bufferSize);
}
}
static void writeToBuffer(char buffer[], size_t bufferSize, size_t iterations) {
cout << oslock << "Writer: ready to write." << endl << osunlock;
for (size_t i = 0; i < iterations * bufferSize; i++) {
char ch = prepareData();
buffer[i % bufferSize] = ch;
cout << oslock << "Writer: published data packet with character '"
<< ch << "'.\t\t" << osunlock;
printBuffer(buffer, bufferSize);
}
}
Goal: we must encode constraints into our program.
What constraint(s) should we add to our program?
How can we model these constraint(s)?
What might this look like in code?
Let's implement a program called myth-buster that prints out how many CS110 student processes are running on each myth machine right now.
myth51 has this many CS110-student processes: 59
myth52 has this many CS110-student processes: 135
myth53 has this many CS110-student processes: 112
myth54 has this many CS110-student processes: 89
myth55 has this many CS110-student processes: 107
myth56 has this many CS110-student processes: 58
myth57 has this many CS110-student processes: 70
myth58 has this many CS110-student processes: 93
myth59 has this many CS110-student processes: 107
myth60 has this many CS110-student processes: 145
myth61 has this many CS110-student processes: 105
myth62 has this many CS110-student processes: 126
myth63 has this many CS110-student processes: 314
myth64 has this many CS110-student processes: 119
myth65 has this many CS110-student processes: 156
myth66 has this many CS110-student processes: 144
Machine least loaded by CS110 students: myth56
Number of CS110 processes on least loaded machine: 58
Let's implement a program called myth-buster that prints out how many CS110 student processes are running on each myth machine right now.
int getNumProcesses(int mythNum, const std::unordered_set<std::string>& sunetIDs);
We'll use the following pre-implemented function that does some networking to fetch process counts. This connects to the specified myth machine, and blocks until done.
Let's implement a program called myth-buster that prints out how many CS110 student processes are running on each myth machine right now.
int main(int argc, char *argv[]) {
// Create a set of student SUNETs
unordered_set<string> cs110SUNETs;
readStudentSUNETsFile(cs110SUNETs, kCS110StudentIDsFile);
// Create a map from myth number -> CS110 process count and print its info
map<int, int> processCountMap;
createCS110ProcessCountMap(cs110SUNETs, processCountMap);
printMythMachineWithFewestProcesses(processCountMap);
return 0;
}
We'll implement createCS110ProcessCountMap sequentially and concurrently.
static void createCS110ProcessCountMap(const unordered_set<string>& sunetIDs,
map<int, int>& processCountMap) {
for (int mythNum = kMinMythMachine; mythNum <= kMaxMythMachine; mythNum++) {
int numProcesses = getNumProcesses(mythNum, sunetIDs);
// If successful, add to the map and print out
if (numProcesses >= 0) {
processCountMap[mythNum] = numProcesses;
cout << "myth" << mythNum << " has this many CS110-student processes: " << numProcesses << endl;
}
}
}
This implementation fetches the count for each myth machine one after the other. This means we have to wait for 16 sequential connections to be started and completed.
Why is this implementation slow?
Each call to getNumProcesses is independent. We should call it multiple times concurrently to overlap this "dead time".
We wait 16 times, because we idle while waiting for a connection to come back.
How can we improve its performance?
What might this look like in code?
Implementation: spawn multiple threads, each responsible for connecting to a different myth machine and updating the map. We'll cap the number of active threads to avoid overloading the myth machines.
Next time: a trip to the ice cream store
For each of the scenarios below, what multithreading patterns might we use to apply appropriate constraints and coordinate threads?
For each of the scenarios below, what multithreading patterns might we use to apply appropriate constraints and coordinate threads?
Challenge: multiple workers periodically need approval from an "approval thread" before continuing. Approval may take some time, and gives back result (approve or deny).
// global struct
struct approval {
mutex available;
int workerData;
semaphore requested;
bool approved;
semaphore finished;
}
// all N workers
// spend time creating data, then...
approval.available.lock();
approval.workerData = ....
approval.requested.signal();
approval.finished.wait();
bool success = approval.approved;
approval.available.unlock();
// approver
while (true) {
approval.requested.wait();
// we are the only one accessing the struct here
approval.approved = someCalculation(approval.workerData);
approval.finished.signal();
}