CS110: Principles of Computer Systems
Winter 2021-2022
Stanford University
Instructors: Nick Troccoli and Jerry Cain
Illustration courtesy of Ecy King, CS110 Champion, Spring 2021
Introduction to Threads
Mutexes and Condition Variables
Semaphores
Multithreading Patterns
We can have concurrency within a single process using threads: independent execution sequences within a single process.
Processes:
Threads:
thread
A thread object can be spawned to run the specified function with the given arguments.
thread myThread(myFunc, arg1, arg2, ...);
thread
For multiple threads, we must wait on a specific thread one at a time:
thread friends[5];
...
for (size_t i = 0; i < 5; i++) {
friends[i].join();
}
To wait on a thread to finish, use the .join() method:
thread myThread(myFunc, arg1, arg2);
... // do some work
// Wait for thread to finish (blocks)
myThread.join();
static const size_t kNumFriends = 6;
static void greeting() {
cout << "Hello, world!" << endl;
}
int main(int argc, char *argv[]) {
cout << "Let's hear from " << kNumFriends << " threads." << endl;
// declare array of empty thread handles
thread friends[kNumFriends];
// Spawn threads
for (size_t i = 0; i < kNumFriends; i++) {
friends[i] = thread(greeting);
}
// Wait for threads
for (size_t i = 0; i < kNumFriends; i++) {
friends[i].join();
}
cout << "Everyone's said hello!" << endl;
return 0;
}
static const size_t kNumFriends = 6;
static void greeting(size_t i) {
cout << "Hello, world! I am thread " << i << endl;
}
int main(int argc, char *argv[]) {
cout << "Let's hear from " << kNumFriends << " threads." << endl;
// declare array of empty thread handles
thread friends[kNumFriends];
// Spawn threads
for (size_t i = 0; i < kNumFriends; i++) {
friends[i] = thread(greeting, i);
}
// Wait for threads
for (size_t i = 0; i < kNumFriends; i++) {
friends[i].join();
}
cout << "Everyone's said hello!" << endl;
return 0;
}
thread
We can also initialize an array of threads as follows (note the loop by reference):
thread friends[5];
for (thread& currFriend : friends) {
currFriend = thread(myFunc, arg1, arg2);
}
// declare array of empty thread handles
thread friends[5];
// Spawn threads
for (size_t i = 0; i < 5; i++) {
friends[i] = thread(myFunc, arg1, arg2);
}
We can make an array of threads as follows:
cout << oslock << "Hello, world!" << endl << osunlock;
static const size_t kNumFriends = 6;
static void greeting(size_t i) {
cout << oslock << "Hello, world! I am thread " << i << endl << osunlock;
}
int main(int argc, char *argv[]) {
cout << "Let's hear from " << kNumFriends << " threads." << endl;
// declare array of empty thread handles
thread friends[kNumFriends];
// Spawn threads
for (size_t i = 0; i < kNumFriends; i++) {
friends[i] = thread(greeting, i);
}
// Wait for threads
for (size_t i = 0; i < kNumFriends; i++) {
friends[i].join();
}
cout << "Everyone's said hello!" << endl;
return 0;
}
static void greeting(size_t& i) {
...
}
for (size_t i = 0; i < kNumFriends; i++) {
friends[i] = thread(greeting, ref(i));
}
for (size_t i = 0; i < kNumFriends; i++) {
friends[i] = thread(greeting, ref(i));
}
_start
greeting
main
argc
argv
i
args
args
args
args
args
args
created thread stacks
main stack
Here, we can just pass by copy instead. But keep an eye out for consequences of shared memory!
int main(int argc, const char *argv[]) {
thread ticketAgents[kNumTicketAgents];
size_t remainingTickets = 250;
for (size_t i = 0; i < kNumTicketAgents; i++) {
ticketAgents[i] = thread(sellTickets, i, ref(remainingTickets));
}
for (thread& ticketAgent: ticketAgents) {
ticketAgent.join();
}
cout << "Ticket selling done!" << endl;
return 0;
}
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
cout << oslock << "Thread #" << id << " sold a ticket (" << remainingTickets
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread #" << id << " sees no remaining tickets to sell and exits."
<< endl << osunlock;
}
thread #1
thread #2
thread #3
remainingTickets = 1
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
cout << oslock << "Thread #" << id << " sold a ticket (" << remainingTickets
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread #" << id << " sees no remaining tickets to sell and exits."
<< endl << osunlock;
}
thread #1
thread #2
thread #3
Line 2: checking if there are tickets left. Yep!
remainingTickets = 1
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
cout << oslock << "Thread #" << id << " sold a ticket (" << remainingTickets
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread #" << id << " sees no remaining tickets to sell and exits."
<< endl << osunlock;
}
thread #1
thread #2
thread #3
Line 2: checking if there are tickets left. Yep!
remainingTickets = 1
z
z
z
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
cout << oslock << "Thread #" << id << " sold a ticket (" << remainingTickets
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread #" << id << " sees no remaining tickets to sell and exits."
<< endl << osunlock;
}
thread #1
thread #2
thread #3
Line 2: checking if there are tickets left. Yep!
remainingTickets = 1
z
z
z
z
z
z
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
cout << oslock << "Thread #" << id << " sold a ticket (" << remainingTickets
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread #" << id << " sees no remaining tickets to sell and exits."
<< endl << osunlock;
}
thread #1
thread #2
thread #3
Line 4: Selling ticket!
remainingTickets = 0
z
z
z
z
z
z
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
cout << oslock << "Thread #" << id << " sold a ticket (" << remainingTickets
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread #" << id << " sees no remaining tickets to sell and exits."
<< endl << osunlock;
}
thread #1
thread #2
thread #3
Line 4: Selling ticket!
remainingTickets = <really large number>
z
z
z
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
cout << oslock << "Thread #" << id << " sold a ticket (" << remainingTickets
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread #" << id << " sees no remaining tickets to sell and exits."
<< endl << osunlock;
}
thread #1
thread #2
thread #3
Line 4: Selling ticket!
remainingTickets = <really large number - 1>
There is a race condition here!
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
...
}
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets-- > 0) {
sleep_for(500); // simulate "selling a ticket"
...
}
// gets remainingTickets
0x0000000000401a9b <+36>: mov -0x20(%rbp),%rax
0x0000000000401a9f <+40>: mov (%rax),%eax
// Decrements by 1
0x0000000000401aa1 <+42>: lea -0x1(%rax),%edx
// Saves updated value
0x0000000000401aa4 <+45>: mov -0x20(%rbp),%rax
0x0000000000401aa8 <+49>: mov %edx,(%rax)
// gets remainingImages
0x0000000000401a9b <+36>: mov -0x20(%rbp),%rax
0x0000000000401a9f <+40>: mov (%rax),%eax
// Decrements by 1
0x0000000000401aa1 <+42>: lea -0x1(%rax),%edx
// Saves updated value
0x0000000000401aa4 <+45>: mov -0x20(%rbp),%rax
0x0000000000401aa8 <+49>: mov %edx,(%rax)
Next time: introducing mutexes