Lecture 13
CS 111: Operating System Principles
Threads
1.0.1
Jon Eyolfson
May 4, 2021
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License
cba
http://creativecommons.org/licenses/by-sa/4.0/
Threads are Like Processes with Shared Memory
The same principle as a process, except by default they share memory
They have their own registers, program counter, and stack
They have the same address space, so changes appear in each thread
You need to explicitly state if any memory is specific to a thread (TLS)
1
One Process Can have Multiple Threads
By default a process just executes code in its own address space
Threads allow multiple executions in the same address space
They’re lighter weight and less expensive to create than processes
They share code, data, file descriptors, etc.
2
Assuming One CPU, Threads Can Express Concurrency
A process can appear like it’s executing in multiple locations at once
However, the OS is just context switching within a process
It may be easier to program concurrently
e.g., handle a web request in a new thread
while (true) {
struct request *req = get_request();
create_thread(process_request, req);
}
3
Threads are Lighter Weight than Processes
Process Thread
Independent code / data / heap Shared code / data / heap
Independent execution Must live within an executing process
Has its own stack and registers Has its own stack and registers
Expensive creation and context switching Cheap creation and context switching
Completely removed from OS on exit Stack removed from process on exit
When a process dies, all threads within it die as well!
4
We’ll be Using POSIX Threads
For Windows, there’s a Win32 thread, but we’re going to use *UNIX threads
#include
-pthread — compile and link the pthread library
All the pthread functions have documentation in the man pages
5
You Create Threads with pthread_create
int pthread_create(pthread_t* thread,
const pthread_attr_t* attr,
void* (*start_routine)(void*),
void* arg);
thread creates a handle to a thread at pointer location
attr thread attributes (NULL for defaults, more details later)
start_routine function to start execution
arg value to pass to start_routine
returns 0 on success, error number otherwise (contents of *thread are undefined)
6
Creating Threads is a Bit Different than Processes
#include
void* run(void*) {
printf(“In run\n”);
return NULL;
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, &run, NULL);
printf(“In main\n”);
}
What are some differences? Are we missing anything?
7
The wait Equivalent for Threads — Join
int pthread_join(pthread_t thread,
void** retval)
thread wait for this thread to terminate (thread must be joinable)
retval stores exit status of thread (set by pthread_exit) to the location
pointed by *retval. If cancelled returns PTHREAD_CANCELED. NULL
is ignored.
returns 0 on success, error number otherwise
Only call this one time per thread!
Multiple calls on the same thread leads to undefined behavior
8
Previous Example that Waits Properly
#include
void* run(void*) {
printf(“In run\n”);
return NULL;
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, &run, NULL);
printf(“In main\n”);
pthread_join(thread, NULL);
}
Now we joined, the thread’s resources are cleaned up
9
Ending a Thread Early (Think of exit)
void pthread_exit(void *retval);
retval return value passed to function that calls pthread_join
Note: start_routine returning is equivalent of calling pthread_exit
Think of the difference between returning from main and exit
pthread_exit is called implicitly when the start_routine of a thread returns
10
Detached Threads
Joinable threads (the default) wait for someone to call pthread_join
then they release their resources
Detached threads release their resources when they terminate
int pthread_detach(pthread_t thread);
thread marks the thread as detached
returns 0 on success, error number otherwise
Calling pthread_detach on an already detached is undefined behavior
11
Detached Threads Aren’t Joined
#include
void* run(void*) {
printf(“In run\n”);
return NULL;
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, &run, NULL);
pthread_detach(thread);
printf(“In main\n”);
}
This code just prints “In main”, why?
12
pthread_exit in main Waits for All Detached Threads to Finish
#include
void* run(void*) {
printf(“In run\n”);
return NULL;
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, &run, NULL);
pthread_detach(thread);
printf(“In main\n”);
pthread_exit(NULL);
}
This code now works as expected
13
You Can Use Attributes To Get/Set Thread Variables
size_t stacksize;
pthread_attr_t attributes;
pthread_attr_init(&attributes);
pthread_attr_getstacksize(&attributes, &stacksize);
printf(“Stack size = %i\n”, stacksize);
pthread_attr_destroy(&attributes);
Running this should show a stack size of 8 MiB (on most Linux systems)
You can also set a thread state to joinable
pthread_attr_setdetachstate(&attributes,
PTHREAD_CREATE_JOINABLE);
14
Multithreading Models
Where do we implement threads?
We can either do user or kernel threads
User threads are completely in user-space
Kernel doesn’t treat your threaded process any differently
Kernel threads are implemented in kernel-space
Kernel manages everything for you, and can treat threads specially
15
Thread Support Requires a Thread Table
Similar to the process table we saw previously
It could be in user-space or kernel-space depending
For user threads, there also needs to be a run-time system to determine scheduling
In both models each process can contain multiple threads
16
We Could Avoid System Calls, or Let a Thread Block Everything
For pure user-level threads (again, no kernel support):
• Very fast to create and destroy, no system call, no context switches
• One thread the blocks blocks the entire process (kernel can’t distinguish)
For kernel-level threads:
• Slower, creation involves system calls
• If one thread blocks, the kernel can schedule another one
17
All Threading Libraries You Use Run in User-mode
The thread library maps user threads to kernel threads
Many-to-one: threads completely implemented in user-space
the kernel only sees one process
One-to-one: one user thread maps directly to one kernel thread
the kernel handles everything
Many-to-many: many user-level threads map to many kernel level threads
18
Many-to-one is Pure User-space Implementation
It’s fast (as outlined before) and portable
It doesn’t depend on the system, it’s just a library
Drawbacks are that one thread blocking causes all threads to block
Also we cannot execute threads in parallel
The kernel will only schedule a process to run
19
One-to-one Just Uses the Kernel Thread Implementation
There’s just a thin wrapper around the system calls to make it easier to use
Exploits the full parallelism of your machine
The kernel can schedule multiple threads simultaneously
We do however need to use a slower system call interface, and we lose some control
Typically this is the actual implementation used, we’ll assume this for Linux
20
Many-to-many is a Hybrid Approach
The idea is that there are more user-level threads than kernel-level threads
Cap the number of kernel-level threads to the number we could run in parallel
We can get the most out of multiple CPUs while reducing the number of system
calls
However, this leads to a complicated thread library
Depending on your mapping luck, you may block other threads
21
Threads Complicate the Kernel
How should fork work with a process with multiple threads?
Copy all threads to the new process, in whatever state they’re in?
How would this get out of hand?
Linux only copies the thread that called fork into a new process
If it hits pthread_exit it’ll always exit with status 0
(at least as far as I can tell)
There’s pthread_atfork (not covered in this course) to control what happens
22
Signals are Sent to a Process
Which thread should receive a signal? all of them?
Linux will just pick one random thread to handle the signal
Makes concurrency hard, any thread could be interrupted
23
Instead of Many-to-many, You Can Use a Thread Pool
The goal of many-to-many thread mapping is to avoid creation costs
A thread pool creates a certain number of threads and a queue of tasks
(maybe as many threads as CPUs in the system)
As requests come in, wake them up and give them work to do
Reuse them, when there’s no work, put them to sleep
24
Our Next Complication
Let’s create a program that spawns 8 threads
Each thread increments the same variable 10,000 times
What should the final value of the variable be?
The initial value of the variable is 0
Run examples/lecture-13/pthread-datarace
Can you fix it?
25
Both Processes and (Kernel) Threads Enable Parallelization
We explored threads, and related them to something we already know (processes)
• Threads are lighter weight, and share memory by default
• Each process can have multiple (kernel) threads
• Most implementations use one-to-one user-to-kernel thread mapping
• The operating system has to manage what happens during a fork, or signals
• We now have synchronization issues
26