Principles of Computer Systems
Winter 2020
Stanford University
Computer Science Department
Instructors: Chris Gregg and
Nick Troccoli
Introduction to Threads
Mutexes and Condition Variables
Condition Variables and Semaphores
Multithreading Patterns
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
static void bat(int unused) {
printf("pirate\n");
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, bat);
pid_t pid = fork();
if (pid == 0) {
printf("ghost\n");
return 0;
}
kill(pid, SIGUSR1);
printf("ninja\n"); return 0;
}
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
static void bat(int unused) {
printf("pirate\n");
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, bat);
pid_t pid = fork();
if (pid == 0) {
printf("ghost\n");
return 0;
}
kill(pid, SIGUSR1);
printf("ninja\n"); return 0;
}
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
int main(int argc, char *argv[]) {
pid_t pid;
int counter = 0;
while (counter < 2) {
pid = fork();
if (pid > 0) break;
counter++;
printf("%d", counter);
}
if (counter > 0) printf("%d", counter);
if (pid > 0) {
waitpid(pid, NULL, 0);
counter += 5;
printf("%d", counter);
}
return 0;
}
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
int main(int argc, char *argv[]) {
pid_t pid;
int counter = 0;
while (counter < 2) {
pid = fork();
if (pid > 0) break;
counter++;
printf("%d", counter);
}
if (counter > 0) printf("%d", counter);
if (pid > 0) {
waitpid(pid, NULL, 0);
counter += 5;
printf("%d", counter);
}
return 0;
}
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
int main(int argc, char *argv[]) {
pid_t pid;
int counter = 0;
while (counter < 2) {
pid = fork();
if (pid > 0) break;
counter++;
printf("%d", counter);
}
if (counter > 0) printf("%d", counter);
if (pid > 0) {
waitpid(pid, NULL, 0);
counter += 5;
printf("%d", counter);
}
return 0;
}
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
int main(int argc, char *argv[]) {
pid_t pid;
int counter = 0;
while (counter < 2) {
pid = fork();
if (pid > 0) break;
counter++;
printf("%d", counter);
}
if (counter > 0) printf("%d", counter);
if (pid > 0) {
waitpid(pid, NULL, 0);
counter += 5;
printf("%d", counter);
}
return 0;
}
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
int main(int argc, char *argv[]) {
pid_t pid;
int counter = 0;
while (counter < 2) {
pid = fork();
if (pid > 0) break;
counter++;
printf("%d", counter);
}
if (counter > 0) printf("%d", counter);
if (pid > 0) {
waitpid(pid, NULL, 0);
counter += 5;
printf("%d", counter);
}
return 0;
}
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
int main(int argc, char *argv[]) {
pid_t pid;
int counter = 0;
while (counter < 2) {
pid = fork();
if (pid > 0) break;
counter++;
printf("%d", counter);
}
if (counter > 0) printf("%d", counter);
if (pid > 0) {
waitpid(pid, NULL, 0);
counter += 5;
printf("%d", counter);
}
return 0;
}
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
int main(int argc, char *argv[]) {
pid_t pid;
int counter = 0;
while (counter < 2) {
pid = fork();
if (pid > 0) break;
counter++;
printf("%d", counter);
}
if (counter > 0) printf("%d", counter);
if (pid > 0) {
waitpid(pid, NULL, 0);
counter += 5;
printf("%d", counter);
}
return 0;
}
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
int main(int argc, char *argv[]) {
pid_t pid;
int counter = 0;
while (counter < 2) {
pid = fork();
if (pid > 0) break;
counter++;
printf("%d", counter);
}
if (counter > 0) printf("%d", counter);
if (pid > 0) {
waitpid(pid, NULL, 0);
counter += 5;
printf("%d", counter);
}
return 0;
}
>
of the counter
> 0
test is changed to a >=
, then counter
values of zeroes would be included in each possible output. How many different outputs are now possible? (No need to list the outputs—just present the number.)Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
int main(int argc, char *argv[]) {
pid_t pid;
int counter = 0;
while (counter < 2) {
pid = fork();
if (pid > 0) break;
counter++;
printf("%d", counter);
}
if (counter > 0) printf("%d", counter);
if (pid > 0) {
waitpid(pid, NULL, 0);
counter += 5;
printf("%d", counter);
}
return 0;
}
>
of the counter
> 0
test is changed to a >=
, then counter
values of zeroes would be included in each possible output. How many different outputs are now possible? (No need to list the outputs—just present the number.) 18 - 6x the original
Consider the following program. Assume that each call to printf flushes its output to the console in full, and further assume that none of the system calls fail in any unpredictable way.
static pid_t pid; // necessarily global so handler1 has access
static int counter = 0;
static void handler1(int unused) {
counter++;
printf("counter = %d\n", counter);
kill(pid, SIGUSR1);
}
static void handler2(int unused) {
counter += 10;
printf("counter = %d\n", counter);
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, handler1);
if ((pid = fork()) == 0) {
signal(SIGUSR1, handler2);
kill(getppid(), SIGUSR1);
while (true) {}
}
if (waitpid(-1, NULL, 0) > 0) {
counter += 1000;
printf("counter = %d\n", counter);
}
return 0;
}
Consider the following program. Assume that each call to printf flushes its output to the console in full, and further assume that none of the system calls fail in any unpredictable way.
static pid_t pid; // necessarily global so handler1 has access
static int counter = 0;
static void handler1(int unused) {
counter++;
printf("counter = %d\n", counter);
kill(pid, SIGUSR1);
}
static void handler2(int unused) {
counter += 10;
printf("counter = %d\n", counter);
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, handler1);
if ((pid = fork()) == 0) {
signal(SIGUSR1, handler2);
kill(getppid(), SIGUSR1);
while (true) {}
}
if (waitpid(-1, NULL, 0) > 0) {
counter += 1000;
printf("counter = %d\n", counter);
}
return 0;
}
counter = 1
counter = 10
counter = 1001
Consider the following program. Assume that each call to printf flushes its output to the console in full, and further assume that none of the system calls fail in any unpredictable way.
static pid_t pid; // necessarily global so handler1 has access
static int counter = 0;
static void handler1(int unused) {
counter++;
printf("counter = %d\n", counter);
kill(pid, SIGUSR1);
}
static void handler2(int unused) {
counter += 10;
printf("counter = %d\n", counter);
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, handler1);
if ((pid = fork()) == 0) {
signal(SIGUSR1, handler2);
kill(getppid(), SIGUSR1);
while (true) {}
}
if (waitpid(-1, NULL, 0) > 0) {
counter += 1000;
printf("counter = %d\n", counter);
}
return 0;
}
Consider the following program. Assume that each call to printf flushes its output to the console in full, and further assume that none of the system calls fail in any unpredictable way.
static pid_t pid; // necessarily global so handler1 has access
static int counter = 0;
static void handler1(int unused) {
counter++;
printf("counter = %d\n", counter);
kill(pid, SIGUSR1);
}
static void handler2(int unused) {
counter += 10;
printf("counter = %d\n", counter);
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, handler1);
if ((pid = fork()) == 0) {
signal(SIGUSR1, handler2);
kill(getppid(), SIGUSR1);
while (true) {}
}
if (waitpid(-1, NULL, 0) > 0) {
counter += 1000;
printf("counter = %d\n", counter);
}
return 0;
}
So, another possible output would be:
counter = 1
counter = 1001
Consider the following program. Assume that each call to printf flushes its output to the console in full, and further assume that none of the system calls fail in any unpredictable way.
static pid_t pid; // necessarily global so handler1 has access
static int counter = 0;
static void handler1(int unused) {
counter++;
printf("counter = %d\n", counter);
kill(pid, SIGUSR1);
}
static void handler2(int unused) {
counter += 10;
printf("counter = %d\n", counter);
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, handler1);
if ((pid = fork()) == 0) {
signal(SIGUSR1, handler2);
kill(getppid(), SIGUSR1);
while (true) {}
}
if (waitpid(-1, NULL, 0) > 0) {
counter += 1000;
printf("counter = %d\n", counter);
}
return 0;
}
Consider the following program. Assume that each call to printf flushes its output to the console in full, and further assume that none of the system calls fail in any unpredictable way.
static pid_t pid; // necessarily global so handler1 has access
static int counter = 0;
static void handler1(int unused) {
counter++;
printf("counter = %d\n", counter);
kill(pid, SIGUSR1);
}
static void handler2(int unused) {
counter += 10;
printf("counter = %d\n", counter);
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, handler1);
if ((pid = fork()) == 0) {
signal(SIGUSR1, handler2);
kill(getppid(), SIGUSR1);
while (true) {}
}
if (waitpid(-1, NULL, 0) > 0) {
counter += 1000;
printf("counter = %d\n", counter);
}
return 0;
}
Consider the following program. Assume that each call to printf flushes its output to the console in full, and further assume that none of the system calls fail in any unpredictable way.
static pid_t pid; // necessarily global so handler1 has access
static int counter = 0;
static void handler1(int unused) {
counter++;
printf("counter = %d\n", counter);
kill(pid, SIGUSR1);
}
static void handler2(int unused) {
counter += 10;
printf("counter = %d\n", counter);
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, handler1);
if ((pid = fork()) == 0) {
signal(SIGUSR1, handler2);
kill(getppid(), SIGUSR1);
while (true) {}
}
if (waitpid(-1, NULL, 0) > 0) {
counter += 1000;
printf("counter = %d\n", counter);
}
return 0;
}
fork
/ execvp
child processes and manage them through the use of signal handlers. It also tests your ability to use pipes.assign4/samples/stsh_soln
)
ls | grep stsh | cut -d- -f2
stsh.cc
A thread is an independent execution sequence within a single process.
Processes:
Threads:
static void greeting() {
cout << oslock << "Hello, world!" << endl << osunlock;
}
static const size_t kNumFriends = 6;
int main(int argc, char *argv[]) {
cout << "Let's hear from " << kNumFriends << " threads." << endl;
thread friends[kNumFriends]; // declare array of empty thread handles
// 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;
}
int main(int argc, const char *argv[]) {
thread processors[10];
size_t remainingImages = 250;
for (size_t i = 0; i < 10; i++)
processors[i] = thread(process, 101 + i, ref(remainingImages));
for (thread& proc: processors) proc.join();
cout << "Images done!" << endl;
return 0;
}
static void process(size_t id, size_t& remainingImages) {
while (remainingImages > 0) {
processImage(remainingImages);
remainingImages--;
cout << oslock << "Thread#" << id << " processed an image (" << remainingImages
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread#" << id << " sees no remaining images and exits."
<< endl << osunlock;
}
myth60 ~../cs110/cthreads -> ./imagethreads
Thread# 109 processed an image, 249 remain
Thread# 102 processed an image, 248 remain
Thread# 101 processed an image, 247 remain
Thread# 104 processed an image, 246 remain
Thread# 108 processed an image, 245 remain
Thread# 106 processed an image, 244 remain
// 241 lines removed for brevity
Thread# 110 processed an image, 3 remain
Thread# 103 processed an image, 2 remain
Thread# 105 processed an image, 1 remain
Thread# 108 processed an image, 0 remain
Thread# 105 processed an image, 18446744073709551615 remain
Thread# 109 processed an image, 18446744073709551614 remain
0x0000000000401a9b <+36>: mov -0x20(%rbp),%rax
0x0000000000401a9f <+40>: mov (%rax),%eax
0x0000000000401aa1 <+42>: lea -0x1(%rax),%edx
0x0000000000401aa4 <+45>: mov -0x20(%rbp),%rax
0x0000000000401aa8 <+49>: mov %edx,(%rax)
class mutex {
public:
mutex(); // constructs the mutex to be in an unlocked state
void lock(); // acquires the lock on the mutex, blocking until it's unlocked
void unlock(); // releases the lock and wakes up another threads trying to lock it
};
static void process(size_t id, size_t& remainingImages, mutex& counterLock) {
while (true) {
counterLock.lock();
if (remainingImages == 0) {
counterLock.unlock();
break;
}
processImage(remainingImages);
remainingImages--;
cout << oslock << "Thread#" << id << " processed an image (" << remainingImages
<< " remain)." << endl << osunlock;
counterLock.unlock();
}
cout << oslock << "Thread#" << id << " sees no remaining images and exits."
<< endl << osunlock;
}
int main(int argc, const char *argv[]) {
size_t remainingImages = 250;
mutex counterLock;
thread processors[10];
for (size_t i = 0; i < 10; i++)
agents[i] = thread(process, 101 + i, ref(remainingImages), ref(counterLock));
for (thread& agent: agents) agent.join();
cout << "Done processing images!" << endl;
return 0;
}
static void process(size_t id, size_t& remainingImages, mutex& counterLock) {
while (true) {
size_t myImage;
counterLock.lock(); // Start of critical section
if (remainingImages == 0) {
counterLock.unlock(); // Rather keep it here, easier to check
break;
} else {
myImage = remainingImages;
remainingImages--;
counterLock.unlock(); // end of critical section
processImage(myImage);
cout << oslock << "Thread#" << id << " processed an image (" << remainingImages
<< " remain)." << endl << osunlock;
}
}
cout << oslock << "Thread#" << id << " sees no remaining images and exits."
<< endl << osunlock;
}