Principles of Computer Systems
Winter 2021
Stanford University
Computer Science Department
Instructors: Chris Gregg and
Nick Troccoli
Introduction to Threads
Mutexes and Condition Variables
Condition Variables and Semaphores
Multithreading Patterns
A thread is an independent execution sequence within a single process.
Processes:
Threads:
pthreads
, which comes with all standard UNIX and Linux installations of gcc, provides thread support, along with other related concurrency directives..static void *recharge(void *args) {
printf("I recharge by spending time alone.\n");
return NULL;
}
static const size_t kNumIntroverts = 6;
int main(int argc, char *argv[]) {
printf("Let's hear from %zu introverts.\n", kNumIntroverts);
pthread_t introverts[kNumIntroverts];
for (size_t i = 0; i < kNumIntroverts; i++)
pthread_create(&introverts[i], NULL, recharge, NULL);
for (size_t i = 0; i < kNumIntroverts; i++)
pthread_join(introverts[i], NULL);
printf("Everyone's recharged!\n");
return 0;
}
pthread_t
handles.pthread_t
(via pthread_create
) by installing recharge
as the thread routine each pthread_t
should execute.void *
and return a void *
. That's the best C can do to support generic programming.pthread_create
is used to set a thread priority and other attributes. We can just pass in NULL
if all threads should have the same priority. That's what we do here.NULL
.recharge
threads is eligible for processor time the instant the surrounding pthread_t
has been initialized.pthread_join
is to threads what waitpid
is to processes.pthread_join
can be used to catch a thread routine's return value. If we don't care to receive it, we can pass in NULL
to ignore it.static const char *kFriends[] = {
"Langston", "Manan", "Edward", "Jordan", "Isabel", "Anne",
"Imaginary"
};
static const size_t kNumFriends = sizeof(kFriends)/sizeof(kFriends[0]) - 1; // count excludes imaginary friend!
static void *meetup(void *args) {
const char *name = kFriends[*(size_t *)args];
printf("Hey, I'm %s. Empowered to meet you.\n", name);
return NULL;
}
int main() {
printf("Let's hear from %zu friends.\n", kNumFriends);
pthread_t friends[kNumFriends];
for (size_t i = 0; i < kNumFriends; i++)
pthread_create(&friends[i], NULL, meetup, &i);
for (size_t j = 0; j < kNumFriends; j++)
pthread_join(friends[j], NULL);
printf("Is everyone accounted for?\n");
return 0;
}
cgregg@myth63$ ./friends
Let's hear from 6 friends.
Hey, I'm Jordan. Empowered to meet you.
Hey, I'm Anne. Empowered to meet you.
Hey, I'm Anne. Empowered to meet you.
Hey, I'm Anne. Empowered to meet you.
Hey, I'm Anne. Empowered to meet you.
Hey, I'm Imaginary. Empowered to meet you.
Is everyone accounted for?
cgregg@myth63$ ./friends
Let's hear from 6 friends.
Hey, I'm Jordan. Empowered to meet you.
Hey, I'm Anne. Empowered to meet you.
Hey, I'm Imaginary. Empowered to meet you.
Hey, I'm Imaginary. Empowered to meet you.
Hey, I'm Imaginary. Empowered to meet you.
Hey, I'm Imaginary. Empowered to meet you.
Is everyone accounted for?
cgregg@myth63$ ./friendsLet's hear from 6 friends.
Hey, I'm Jordan. Empowered to meet you.
Hey, I'm Isabel. Empowered to meet you.
Hey, I'm Isabel. Empowered to meet you.
Hey, I'm Anne. Empowered to meet you.
Hey, I'm Anne. Empowered to meet you.
Hey, I'm Imaginary. Empowered to meet you.
Is everyone accounted for?
meetup
references its incoming parameter now, and that pthread_create
accepts the address of the surrounding loop's index variable i
via its fourth parameter. pthread_create
's fourth argument is always passed verbatim as the single argument to the thread routine.i
without regard for the fact that i
's address was shared with six child threads.
i
, but the value of i
itself. That assumption of course, is incorrect, as it captures the address and nothing else.i
(even after it goes out of scope) is constant, but its contents evolve in parallel with the execution of the six meetup
threads. *(size_t *)args
takes a snapshot of whatever i
happens to contain at the time it's evaluated.meetup
threads only execute after the main thread has worked through all of its first for loop. The space at &i
is left with a 6, and that's why Imaginary is printed so often.const char *
instead. Snapshots of the const char *
pointers are passed verbatim to meetup
. The strings themselves are constants.static const char *kFriends[] = {
"Langston", "Manan", "Edward", "Jordan", "Isabel", "Anne",
"Imaginary"
};
static const size_t kNumFriends = sizeof(kFriends)/sizeof(kFriends[0]) - 1; // count excludes imaginary friend!
static void *meetup(void *args) {
const char *name = args; // note that we get the name directly instead of through an index
printf("Hey, I'm %s. Empowered to meet you.\n", name);
return NULL;
}
int main() {
printf("%zu friends meet.\n", kNumFriends);
pthread_t friends[kNumFriends];
for (size_t i = 0; i < kNumFriends; i++)
pthread_create(&friends[i], NULL, meetup, (void *) kFriends[i]); // this line now provides a variable that won't change
for (size_t i = 0; i < kNumFriends; i++)
pthread_join(friends[i], NULL);
printf("All friends are real!\n");
return 0;
}
cgregg@myth63$ ./friends
Let's hear from 6 friends.
Hey, I'm Langston. Empowered to meet you.
Hey, I'm Manan. Empowered to meet you.
Hey, I'm Edward. Empowered to meet you.
Hey, I'm Jordan. Empowered to meet you.
Hey, I'm Isabel. Empowered to meet you.
Hey, I'm Anne. Empowered to meet you.
Is everyone accounted for?
cgregg@myth63$ ./friends
Let's hear from 6 friends.
Hey, I'm Langston. Empowered to meet you.
Hey, I'm Manan. Empowered to meet you.
Hey, I'm Edward. Empowered to meet you.
Hey, I'm Isabel. Empowered to meet you.
Hey, I'm Jordan. Empowered to meet you.
Hey, I'm Anne. Empowered to meet you.
Is everyone accounted for?
cgregg@myth63$ ./friends
Let's hear from 6 friends.
Hey, I'm Langston. Empowered to meet you.
Hey, I'm Edward. Empowered to meet you.
Hey, I'm Manan. Empowered to meet you.
Hey, I'm Anne. Empowered to meet you.
Hey, I'm Jordan. Empowered to meet you.
Hey, I'm Isabel. Empowered to meet you.
Is everyone accounted for?
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 (thread &f : friends) {
f.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
static void process(size_t id, size_t& remainingImages) {
while (remainingImages > 0) {
remainingImages--;
processImage(remainingImages);
cout << oslock << "Thread#" << id << " processed an image (" << remainingImages
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread#" << id << " sees no remaining images and exits."
<< endl << osunlock;
}
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++)
processors[i] = thread(process, 101 + i, ref(remainingImages), ref(counterLock));
for (thread& process: processes) process.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;
}