mirror of
https://github.com/cs341-illinois/coursebook.git
synced 2026-10-29 07:13:52 +08:00
654 lines
27 KiB
TeX
654 lines
27 KiB
TeX
\chapter{Threads}
|
|
|
|
\epigraph{If you think your programs were not working before, just wait until they crash ten times as fast}{Bhuvy}
|
|
|
|
A thread is short for `thread-of-execution'.
|
|
It represents the sequence of instructions that the CPU has and will execute.
|
|
To remember how to return from function calls, and to store the values of automatic variables and parameters a thread uses a stack.
|
|
Almost weirdly, a thread is a process, meaning that creating a thread is similar to \keyword{fork}, except there is \textbf{no copying} meaning no copy on write.
|
|
What this allows is for a process to share the same address space, variables, heap, file descriptors and etc.
|
|
The actual system call to create a thread is similar to \keyword{fork}. It's \keyword{clone}.
|
|
We won't go into the specifics but you can read the \href{http://man7.org/linux/man-pages/man2/clone.2.html}{man pages} keeping in mind that it is outside the direct scope of this course.
|
|
LWP or Lightweight Processes or threads are preferred to forking for a lot of scenarios because there is a lot less overhead creating them.
|
|
But in some cases, notably python uses this, multiprocessing is the way to make your code faster.
|
|
|
|
\section{Processes vs threads}
|
|
|
|
Creating separate processes is useful when
|
|
|
|
\begin{itemize}
|
|
\item When more security is desired. For example, Chrome browser uses different processes for different tabs.
|
|
\item When running an existing and complete program then a new process is required, for example starting `gcc'.
|
|
\item When you are running into synchronization primitives and each process is operating on something in the system.
|
|
\item When you have too many threads -- the kernel tries to schedule all the threads near each other which could cause more harm than good.
|
|
\item When you don't want to worry about race conditions
|
|
\item If one thread blocks in a task (say IO) then all threads block. Processes don't have that same restriction.
|
|
\item When the amount of communication is minimal enough that simple IPC needs to be used.
|
|
\end{itemize}
|
|
|
|
On the other hand, creating threads is more useful when
|
|
\begin{itemize}
|
|
\item You want to leverage the power of a multi-core system to do one task
|
|
\item When you can't deal with the overhead of processes
|
|
\item When you want communication between the processes simplified
|
|
\item When you want to threads to be part of the same process
|
|
\end{itemize}
|
|
|
|
\section{Thread Internals}
|
|
|
|
Your main function and other functions you might call has automatic variables.
|
|
We will store them in memory using a stack and keep track of how large the stack is by using a simple pointer (the ``stack pointer'').
|
|
If the thread calls another function, we move our stack pointer down, so that we have more space for parameters and automatic variables.
|
|
Once it returns from a function, we can move the stack pointer back up to its previous value.
|
|
We keep a copy of the old stack pointer value - on the stack!
|
|
This is why returning from a function is very quick - it's easy to `free' the memory used by automatic variables - we just need to change the stack pointer.
|
|
|
|
In a multi threaded program, there are multiple stack but only one address space. The pthread library allocates some stack space and uses the \keyword{clone} function call to start the thread at that stack address.
|
|
|
|
\begin{figure}[H]
|
|
\centering
|
|
\includegraphics[width=.5\textwidth]{threads/drawings/thread_stack.png}
|
|
\caption{Thread stack visualization}
|
|
\end{figure}
|
|
|
|
|
|
You can have more than one thread running inside a process.
|
|
You get the first thread for free! It runs the code you write inside `main'.
|
|
If you need more threads you can call \keyword{pthread\_create} to create a new thread using the pthread library. You'll need to pass a pointer to a function so that the thread knows where to start.
|
|
|
|
The threads you create all live inside the same virtual memory because they are part of the same process.
|
|
Thus they can all see the heap, the global variables and the program code etc.
|
|
|
|
\begin{figure}[H]
|
|
\centering
|
|
\includegraphics[width=.5\textwidth]{threads/drawings/thread_stack_pointer.png}
|
|
\caption{Threads pointing to same place in heap}
|
|
\end{figure}
|
|
|
|
Thus you can have two (or more) CPUs working on your program at the same time and inside the same process.
|
|
It's up to the operating system to assign the threads to CPUs.
|
|
If you have more active threads than CPUs, the kernel will assign the thread to a CPU for a short duration or until it runs out of things to do and then will automatically switch the CPU to work on another thread.
|
|
For example, one CPU might be processing the game AI while another thread is computing the graphics output.
|
|
|
|
\section{Simple Usage}
|
|
|
|
To use pthreads you will need to include \keyword{pthread.h} and compile and link with \keyword{-pthread} or \keyword{-lpthread} compiler option.
|
|
This option tells the compiler that your program requires threading support.
|
|
To create a thread use the function \keyword{pthread\_create}. This function takes four arguments:
|
|
|
|
\begin{lstlisting}[language=C]
|
|
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
|
|
void *(*start_routine) (void *), void *arg);
|
|
\end{lstlisting}
|
|
|
|
\begin{itemize}
|
|
\tightlist
|
|
\item
|
|
The first is a pointer to a variable that will hold the id of the newly created thread.
|
|
\item
|
|
The second is a pointer to attributes that we can use to tweak and tune some of the advanced features of pthreads.
|
|
\item
|
|
The third is a pointer to a function that we want to run
|
|
\item
|
|
Fourth is a pointer that will be given to our function
|
|
\end{itemize}
|
|
|
|
The argument \keyword{void *(*start\_routine) (void *)} is difficult to read!
|
|
It means a pointer that takes a \keyword{void *} pointer and returns a \keyword{void *} pointer.
|
|
It looks like a function declaration except that the name of the function is wrapped with \keyword{(* .... )}
|
|
|
|
\begin{lstlisting}[language=C]
|
|
#include <stdio.h>
|
|
#include <pthread.h>
|
|
|
|
void *busy(void *ptr) {
|
|
// ptr will point to "Hi"
|
|
puts("Hello World");
|
|
return NULL;
|
|
}
|
|
int main() {
|
|
pthread_t id;
|
|
pthread_create(&id, NULL, busy, "Hi");
|
|
void *result;
|
|
pthread_join(id, &result);
|
|
}
|
|
\end{lstlisting}
|
|
|
|
In the above example, \keyword{result} will be \keyword{null} because the busy function returned \keyword{null}.
|
|
We need to pass the address-of result because \keyword{pthread\_join} will be writing into the contents of our pointer.
|
|
|
|
In the man pages, it warns that programmers should use \keyword{pthread\_t} as an opaque type and not look at the internals.
|
|
We do ignore that often, though.
|
|
|
|
\section{Pthread Functions}
|
|
|
|
Here are some common pthread functions to help you get going.
|
|
|
|
\begin{itemize}
|
|
\item \keyword{pthread\_create}.
|
|
Creates a new thread.
|
|
Every thread that gets created gets a new stack.
|
|
If you call \keyword{pthread\_create} twice, Your process will contain three stacks - one for each thread.
|
|
The first thread is created when the process starts, and you created two more.
|
|
Actually there can be more stacks than this, but let's keep it simple.
|
|
The important idea is that each thread requires a stack because the stack contains automatic variables and the old CPU PC register, so that it can go back to executing the calling function after the function is finished.
|
|
|
|
\item \keyword{pthread\_cancel} stops a thread.
|
|
Note the thread may not actually be stopped immediately.
|
|
For example it can be terminated when the thread makes an operating system call (e.g. \keyword{write}).
|
|
In practice, \keyword{pthread\_cancel} is rarely used because it does not give a thread an opportunity to clean up after itself (for example, it may have opened some files).
|
|
An alternative implementation is to use a boolean (int) variable whose value is used to inform other threads that they should finish and clean up.
|
|
|
|
\item \keyword{pthread\_exit(void *)} stops the calling thread meaning the thread never returns after calling \keyword{pthread\_exit}.
|
|
The pthread library will automatically finish the process if there are no other threads running.
|
|
\keyword{pthread\_exit(...)} is equivalent to returning from the thread's function; both finish the thread and also set the return value (void *pointer) for the thread.
|
|
Calling \keyword{pthread\_exit} in the \keyword{main} thread is a common way for simple programs to ensure that all threads finish.
|
|
For example, in the following program, the \keyword{myfunc} threads will probably not have time to get started.
|
|
On the other hand \keyword{exit()} exits the entire process and sets the process' exit value.
|
|
This is equivalent to \keyword{return ();} in the main method.
|
|
All threads inside the process are stopped.
|
|
Note the \keyword{pthread\_exit} version creates thread zombies, however this is not a long-running process, so we don't care.
|
|
|
|
\begin{lstlisting}[language=C]
|
|
int main() {
|
|
pthread_t tid1, tid2;
|
|
pthread_create(&tid1, NULL, myfunc, "Jabberwocky");
|
|
pthread_create(&tid2, NULL, myfunc, "Vorpel");
|
|
if (keep_threads_going) {
|
|
pthread_exit(NULL);
|
|
} else {
|
|
exit(42); //or return 42;
|
|
}
|
|
|
|
// No code is run after exit
|
|
}
|
|
\end{lstlisting}
|
|
|
|
\item \keyword{pthread\_join()} waits for a thread to finish and records its return value.
|
|
Finished threads will continue to consume resources.
|
|
Eventually, if enough threads are created, \keyword{pthread\_create} will fail.
|
|
In practice, this is only an issue for long-running processes but is not an issue for simple, short-lived processes as all thread resources are automatically freed when the process exits.
|
|
This is equivalent to turning your children into zombies, so keep this in mind for long running processes. In the exit example, we could also wait on all the threads.
|
|
|
|
\begin{lstlisting}[language=C]
|
|
// ...
|
|
void* result;
|
|
pthread_join(tid1, &result);
|
|
pthread_join(tid2, &result);
|
|
return 42;
|
|
// ...
|
|
\end{lstlisting}
|
|
|
|
|
|
\end{itemize}
|
|
|
|
There are many ways to exit threads.
|
|
Here is a non-complete list.
|
|
|
|
\begin{itemize}
|
|
\item Returning from the thread function
|
|
\item Calling \keyword{pthread\_exit}
|
|
\item Cancelling the thread with \keyword{pthread\_cancel}
|
|
\item Terminating the process through a signal.
|
|
\item calling \keyword{exit()} or \keyword{abort()}
|
|
\item Returning from \keyword{main}
|
|
\item \keyword{exec}'ing another program
|
|
\item Unplugging your computer
|
|
\item Some undefined behavior can terminate your threads, it is undefined behavior
|
|
\end{itemize}
|
|
|
|
\section{Race Conditions}
|
|
|
|
Race conditions are whenever the outcome of a program is determined by its sequence of events determined by the processor.
|
|
This means that the execution of the code is non-deterministic.
|
|
Meaning that the same program can run multiple times and depending on how the kernel schedules the threads could produce inaccurate results.
|
|
The following is the canonical race condition.
|
|
|
|
\begin{lstlisting}[language=C]
|
|
void *thread_main(void *p) {
|
|
int x = *p;
|
|
x += x;
|
|
*p = x;
|
|
return NULL;
|
|
}
|
|
|
|
int main() {
|
|
int data = 1;
|
|
pthread_t one, two;
|
|
pthread_create(&one, NULL, thread_main, &data);
|
|
pthread_create(&two, NULL, thread_main, &data);
|
|
pthread_join(one, NULL);
|
|
pthread_join(two, NULL);
|
|
printf("%d\n", data);
|
|
return 0;
|
|
}
|
|
\end{lstlisting}
|
|
|
|
Breaking down the assembly there are many different accesses of the code.
|
|
We will assume that data is stored in the \keyword{eax} register.
|
|
The code to increment is the following with no optimization (assume int\_ptr contains eax).
|
|
|
|
\begin{lstlisting}[language={[x86masm]Assembler}]
|
|
mov eax, DWORD PTR [rbp-4] ;Loads int_ptr
|
|
add eax, eax ;Does the addition
|
|
mov DWORD PTR [rbp-4], eax ;Stores it back
|
|
\end{lstlisting}
|
|
|
|
Consider this access pattern.
|
|
|
|
\begin{figure}[H]
|
|
\centering
|
|
\includegraphics[width=.7\textwidth]{threads/drawings/thread_nonrace_timing.png}
|
|
\caption{Thread access - not a race condition}
|
|
\end{figure}
|
|
|
|
This access pattern will cause the variable \keyword{data} to be 4.
|
|
The problem is when the instructions are executed in parallel.
|
|
|
|
\begin{figure}[H]
|
|
\centering
|
|
\includegraphics[width=.5\textwidth]{threads/drawings/thread_race_timing.png}
|
|
\caption{Thread access - race condition}
|
|
\end{figure}
|
|
|
|
This access pattern will cause the variable \keyword{data} to be 2.
|
|
This is undefined behavior and a race condition.
|
|
What we want is one thread to access the part of the code at a time.
|
|
|
|
You may also ask, well when I compile the code with \keyword{-O2} then I get
|
|
|
|
\begin{lstlisting}[language={[x86masm]Assembler}]
|
|
shl dword ptr [rdi] # Optimized way of doing the add
|
|
\end{lstlisting}
|
|
|
|
Shouldn't that fix it? It is a single assembly instructions so no interleaving?
|
|
Well it doesn't fix the problems that the \textit{hardware itself} may experience a race condition because we as programmers didn't tell the hardware to check for it.
|
|
The easiest way is to add the \textit{lock} prefix \cite[p. 1120]{guide2011intel}.
|
|
|
|
But we don't want to be coding in assembly!
|
|
We need to come up with a software solution to this problem.
|
|
|
|
\subsubsection{A day at the races}
|
|
|
|
Here is another small race condition.
|
|
The following code is supposed to start ten threads with the integers 0 through 9 inclusive.
|
|
However, when run prints out \keyword{1 7 8 8 8 8 8 8 8 10}!
|
|
Or very seldom does it print out what we expect.
|
|
Can you see why?
|
|
|
|
\begin{lstlisting}[language=C]
|
|
#include <pthread.h>
|
|
void* myfunc(void* ptr) {
|
|
int i = *((int *) ptr);
|
|
printf("%d ", i);
|
|
return NULL;
|
|
}
|
|
|
|
int main() {
|
|
// Each thread gets a different value of i to process
|
|
int i;
|
|
pthread_t tid;
|
|
for(i =0; i < 10; i++) {
|
|
pthread_create(&tid, NULL, myfunc, &i); // ERROR
|
|
}
|
|
pthread_exit(NULL);
|
|
}
|
|
\end{lstlisting}
|
|
|
|
The above code suffers from a \keyword{race condition} - the value of i is changing.
|
|
The new threads start later in the example output the last thread starts after the loop has finished.
|
|
To overcome this race-condition, we will give each thread a pointer to its own data area.
|
|
For example, for each thread we may want to store the id, a starting value and an output value.
|
|
We will instead treat i as a pointer and cast it by value.
|
|
|
|
\begin{lstlisting}[language=C]
|
|
void* myfunc(void* ptr) {
|
|
int data = ((int) ptr);
|
|
printf("%d ", data);
|
|
return NULL;
|
|
}
|
|
|
|
int main() {
|
|
// Each thread gets a different value of i to process
|
|
int i;
|
|
pthread_t tid;
|
|
for(i =0; i < 10; i++) {
|
|
pthread_create(&tid, NULL, myfunc, (void *)i);
|
|
}
|
|
pthread_exit(NULL);
|
|
}
|
|
\end{lstlisting}
|
|
|
|
Race conditions aren't just in our code.
|
|
they can be in provided code
|
|
Some functions like \keyword{asctime}, \keyword{getenv}, \keyword{strtok}, \keyword{strerror} not thread-safe.
|
|
Let's look at a simple function that is also not `thread-safe'.
|
|
The result buffer could be stored in global memory.
|
|
This is good in a single threaded program.
|
|
We wouldn't want to return a pointer to an invalid address on the stack, but there's only one result buffer in the entire memory.
|
|
If two threads were to use it at the same time, one would corrupt the other.
|
|
|
|
\begin{lstlisting}[language=C]
|
|
char *to_message(int num) {
|
|
static char result [256];
|
|
if (num < 10) sprintf(result, "%d : blah blah" , num);
|
|
else strcpy(result, "Unknown");
|
|
return result;
|
|
}
|
|
\end{lstlisting}
|
|
|
|
There are ways around this like using synchronization locks, but first let's do this by design.
|
|
How would you fix the function above?
|
|
You can change any of the parameters and any return types.
|
|
Here is one valid solution.
|
|
|
|
\begin{lstlisting}[language=C]
|
|
int to_message_r(int num, char *buf, size_t nbytes) {
|
|
size_t written;
|
|
if (num < 10) {
|
|
written = snprintf(buf, nbtytes, "%d : blah blah" , num);
|
|
} else {
|
|
strncpy(buf, "Unknown", nbytes);
|
|
buf[nbytes] = '\0';
|
|
written = strlen(buf) + 1;
|
|
}
|
|
return written <= nbytes;
|
|
}
|
|
\end{lstlisting}
|
|
|
|
Instead of making the function responsible for the memory, we made the caller responsible!
|
|
A lot of programs, and hopefully your programs, have minimal communication needed.
|
|
Often a malloc call is much less work than locking a mutex or sending a message to another thread.
|
|
|
|
\subsection{Don't Cross the Streams}
|
|
|
|
In case you were wondering, you can fork inside a process with multiple threads!
|
|
However, the child process only has a single thread, which is a clone of the thread that called \keyword{fork}.
|
|
We can see this as a simple example, where the background threads never print out a second message in the child process.
|
|
|
|
\begin{lstlisting}[language=C]
|
|
#include <pthread.h>
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
|
|
static pid_t child = -2;
|
|
|
|
void *sleepnprint(void *arg) {
|
|
printf("%d:%s starting up...\n", getpid(), (char *) arg);
|
|
|
|
while (child == -2) {sleep(1);} /* Later we will use condition variables */
|
|
|
|
printf("%d:%s finishing...\n",getpid(), (char*)arg);
|
|
|
|
return NULL;
|
|
}
|
|
int main() {
|
|
pthread_t tid1, tid2;
|
|
pthread_create(&tid1,NULL, sleepnprint, "New Thread One");
|
|
pthread_create(&tid2,NULL, sleepnprint, "New Thread Two");
|
|
|
|
child = fork();
|
|
printf("%d:%s\n",getpid(), "fork()ing complete");
|
|
sleep(3);
|
|
|
|
printf("%d:%s\n",getpid(), "Main thread finished");
|
|
|
|
pthread_exit(NULL);
|
|
return 0; /* Never executes */
|
|
}
|
|
\end{lstlisting}
|
|
|
|
\begin{verbatim}
|
|
8970:New Thread One starting up...
|
|
8970:fork()ing complete
|
|
8973:fork()ing complete
|
|
8970:New Thread Two starting up...
|
|
8970:New Thread Two finishing...
|
|
8970:New Thread One finishing...
|
|
8970:Main thread finished
|
|
8973:Main thread finished
|
|
\end{verbatim}
|
|
|
|
In practice, creating threads before forking can lead to unexpected errors because (as demonstrated above) the other threads are immediately terminated when forking.
|
|
Another thread might have just lock a mutex like by calling malloc and never unlock it again.
|
|
Advanced users may find \keyword{pthread\_atfork} useful however we suggest you usually try to avoid creating threads before forking unless you fully understand the limitations and difficulties of this approach.
|
|
|
|
\subsection{Embarrassingly Parallel Problems}
|
|
|
|
The study of parallel algorithms has exploded over the past few years.
|
|
An embarrassingly parallel problem is any problem that needs little effort to turn parallel.
|
|
A lot of them have some synchronization concepts with them but not always.
|
|
You already know a parallelizable algorithm, Merge Sort!
|
|
|
|
\begin{lstlisting}[language=C]
|
|
void merge_sort(int *arr, size_t len){
|
|
if(len > 1){
|
|
// Merge Sort the left half
|
|
// Merge Sort the right half
|
|
// Merge the two halves
|
|
}
|
|
\end{lstlisting}
|
|
|
|
With your new understanding of threads, all you need to do is create a thread for the left half, and one for the right half.
|
|
Given that your CPU has multiple real cores, you will see a speedup in accordance with \href{https://en.wikipedia.org/wiki/Amdahl's_law}{Amdahl's Law}.
|
|
The time complexity analysis gets interesting here as well.
|
|
The parallel algorithm runs in $O(\log^3(n))$ running time because we have the analysis assumes that we have a lot of cores.
|
|
|
|
In practice though, we typically do two changes.
|
|
One, once the array gets small enough, we ditch the parallel Merge Sort algorithm and do a quicksort or other algorithm that works fast on small arrays, usually cache coherency rules at this level.
|
|
The other thing that we know is that CPUs don't have infinite cores.
|
|
To get around that, we typically keep a worker pool.
|
|
You won't see the speedup right away because of things like cache coherency and scheduling extra threads.
|
|
Over the long term though, you will start to see speedups.
|
|
|
|
Another embarrassingly parallel problem is parallel map.
|
|
Say we want to apply a function to an entire array, one element at a time.
|
|
|
|
\begin{lstlisting}[language=C]
|
|
int *map(int (*func)(int), int *arr, size_t len){
|
|
int *ret = malloc(len*sizeof(*arr));
|
|
for(size_t i = 0; i < len; ++i) {
|
|
ret[i] = func(arr[i]);
|
|
}
|
|
return ret;
|
|
}
|
|
\end{lstlisting}
|
|
|
|
Since none of the elements depend on any other element, how would you go about parallelizing this?
|
|
What do you think would be the best way to split up the work between threads.
|
|
|
|
\subsection{Extra: Scheduling}
|
|
|
|
There are a few ways to split up the work.
|
|
These are common to the OpenMP framework \cite{silberschatz2005operating}.
|
|
|
|
\begin{itemize}
|
|
\item \keyword{static scheduling} break up the problems into fixed size chunks (predetermined) and have each thread work on each of the chunks.
|
|
This works well when each of the subproblems take roughly the same time because there is no additional overhead.
|
|
All you need to do is write a loop and give the map function to each sub-array.
|
|
\item \keyword{dynamic scheduling} as a new problem becomes available have a thread serve it.
|
|
This is useful when you don't know how long the scheduling will take
|
|
\item \keyword{guided scheduling} This is a mix of the above with a mix of the benefits and tradeoffs.
|
|
You start with a static scheduling and move slowly to dynamic if needed
|
|
\item \keyword{runtime scheduling} You have absolutely no idea how long the problems are going to take.
|
|
Instead of deciding it yourself, let the program decide what to do!
|
|
\end{itemize}
|
|
|
|
No need to memorize any of the scheduling routines though.
|
|
Openmp is a standard that is an alternative to pthreads.
|
|
For example here is how to parallelize a for loop
|
|
|
|
\begin{lstlisting}[language=C]
|
|
#pragma omp parallel for
|
|
for (int i = 0; i < n; i++) {
|
|
// Do stuff
|
|
}
|
|
|
|
// Specify the scheduling as follows
|
|
// #pragma omp parallel for scheduling(static)
|
|
\end{lstlisting}
|
|
|
|
Static scheduling will divide the problem into fixed size chunks
|
|
Dynamic scheduling will give a job once the loop is over
|
|
Guided scheduling is Dynamic with chunks
|
|
Runtime is a whole bag of worms.
|
|
|
|
\subsection{Other Problems}
|
|
|
|
From \href{https://en.wikipedia.org/wiki/Embarrassingly_parallel}{Wikipedia}
|
|
\begin{itemize}
|
|
\tightlist
|
|
\item Serving static files on a web server to multiple users at once.
|
|
\item The Mandelbrot set, Perlin noise and similar images, where each point is calculated independently.
|
|
\item Rendering of computer graphics. In computer animation, each frame may be rendered independently (see parallel rendering).
|
|
\item Brute-force searches in cryptography.
|
|
\item Notable real-world examples include distributed.net and proof-of-work systems used in cryptocurrency.
|
|
\item BLAST searches in bioinformatics for multiple queries (but not for individual large queries)
|
|
\item Large scale facial recognition systems that compare thousands of arbitrary acquired faces (e.g., a security or surveillance video via closed-circuit television) with similarly large number of previously stored faces (e.g., a rogues gallery or similar watch list).
|
|
\item Computer simulations comparing many independent scenarios, such as climate models.
|
|
\item Evolutionary computation meta-heuristics such as genetic algorithms.
|
|
\item Ensemble calculations of numerical weather prediction.
|
|
\item Event simulation and reconstruction in particle physics.
|
|
\item The marching squares algorithm
|
|
\item Sieving step of the quadratic sieve and the number field sieve.
|
|
\item Tree growth step of the random forest machine learning technique.
|
|
\item Discrete Fourier Transform where each harmonic is independently calculated.
|
|
\end{itemize}
|
|
|
|
\subsection{Extra: threads.h}
|
|
|
|
We have a lot of threading libraries discussed in the extra section.
|
|
We have the standard POSIX threads, OpenMP threads, we also have a new C11 threading library that is built into the standard.
|
|
This library provides restricted functionality.
|
|
|
|
You ask yourself, why would I use the restricted functionality?
|
|
The key is in the name.
|
|
Since this is the C standard library, it has to be implemented on all operating systems that are compliant which are pretty much all of them.
|
|
This means there is first-class portability when using threads.
|
|
|
|
We won't drone on about the functions.
|
|
Most of them are just renaming of pthread functions anyway.
|
|
If you ask why we don't teach these, there are a few reasons
|
|
|
|
\begin{enumerate}
|
|
\item They are pretty new. Even though the standard came out in roughly 2011, POSIX threads have been around forever.
|
|
A lot of their quirks have been ironed out.
|
|
\item You lose expressivity.
|
|
This is a concept that we'll talk about in layer chapters, but when you make something portable, you lose some expressivity with the host hardware.
|
|
That means that the threads.h library is pretty bare bones.
|
|
It is hard to set CPU affinities.
|
|
Schedule threads together.
|
|
Efficiently look at the internals for performance reasons.
|
|
\item A lot of legacy code is already written with POSIX threads in mind.
|
|
Other libraries like OpenMP, CUDA, MPI will either use POSIX processes or POSIX threads with a begrudging port to Windows.
|
|
\end{enumerate}
|
|
|
|
\subsection{Extra: Lightweight Processes?}
|
|
|
|
Earlier in the beginning of the chapter we mentioned that threads are just processes.
|
|
What do we mean by that?
|
|
You can create a thread just like a process
|
|
Take a look at the example code below
|
|
|
|
\begin{lstlisting}[language=C]
|
|
|
|
// 8 KiB stacks
|
|
#define STACK_SIZE (8 * 1024 * 1024)
|
|
|
|
int thread_start(void *arg) {
|
|
// Just like the pthread function
|
|
puts("Hello Clone!")
|
|
// I share the same heap and address space!
|
|
return 0;
|
|
}
|
|
|
|
int main() {
|
|
// Allocate stack space for the child
|
|
char *child_stack = malloc(STACK_SIZE);
|
|
// Remember stacks work by growing down, so we need
|
|
// to give the top of the stack
|
|
char *stack_top = stack + STACK_SIZE;
|
|
|
|
// clone create thread
|
|
pid_t pid = clone(thread_start, stack_top, SIGCHLD, NULL);
|
|
if (pid == -1) {
|
|
perror("clone");
|
|
exit(1);
|
|
}
|
|
printf("Child pid %ld\n", (long) pid);
|
|
|
|
// Wait just like any child
|
|
if (waitpid(pid, NULL, 0) == -1) {
|
|
perror("waitpid");
|
|
exit(1);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
\end{lstlisting}
|
|
|
|
Seems pretty simple right?
|
|
Well why don't we have you do it this way every time?
|
|
First there is a decent bit of boilerplate code.
|
|
You have to create the stack every time and pass in the right flags, and wait like a process which requires a lot of finicky flags to be set from the man pages.
|
|
In addition, almost everyone uses pthreads by default.
|
|
Pthreads let you set various attributes -- some that resemble the option in clone -- to customize your thread.
|
|
But as we mentioned earlier, with each later of abstraction for portability reasons we lose some functionality.
|
|
clone can do some neat things like keep different parts of your heap the same while creating copies of other pages.
|
|
You have finer linux control of scheduling because it is a process, just with the same mappings.
|
|
|
|
At no time in this course should you be using clone.
|
|
But in the future, know that it is a perfectly viable alternative to fork.
|
|
You just have to be careful about what you are doing.
|
|
|
|
\subsection{Further Reading}
|
|
|
|
\begin{itemize}
|
|
\item \href{http://man7.org/linux/man-pages/man3/pthread_create.3.html}{man page}
|
|
\item \href{http://man7.org/linux/man-pages/man7/pthreads.7.html}{pthread reference guide}
|
|
\item \href{http://www.thegeekstuff.com/2012/04/terminate-c-thread/}{Concise third party sample code explaining create, join and exit}
|
|
\end{itemize}
|
|
|
|
\section{Topics}
|
|
|
|
\begin{itemize}
|
|
\tightlist
|
|
\item
|
|
pthread life-cycle
|
|
\item
|
|
Each thread has a stack
|
|
\item
|
|
Capturing return values from a thread
|
|
\item
|
|
Using \keyword{pthread\_join}
|
|
\item
|
|
Using \keyword{pthread\_create}
|
|
\item
|
|
Using \keyword{pthread\_exit}
|
|
\item
|
|
Under what conditions will a process exit
|
|
\end{itemize}
|
|
|
|
\section{Questions}
|
|
|
|
\begin{itemize}
|
|
\tightlist
|
|
\item
|
|
What happens when a pthread gets created?
|
|
\item
|
|
Where is each thread's stack?
|
|
\item
|
|
How do you get a return value given a \keyword{pthread\_t}? What are the ways a thread can set that return value? What happens if you discard the return value?
|
|
\item
|
|
Why is \keyword{pthread\_join} important (think stack space, registers, return values)?
|
|
\item
|
|
What does \keyword{pthread\_exit} do under normal circumstances (i.e. you are not the last thread)? What other functions are called when you call pthread\_exit?
|
|
\item
|
|
Give me three conditions under which a multithreaded process will exit. Can you think of any more?
|
|
\item
|
|
What is an embarrassingly parallel problem?
|
|
\end{itemize}
|
|
|
|
\bibliographystyle{plainnat}
|
|
\bibliography{threads/threads}
|