lecture-notes/inputs/processes-and-threads/threads.tex
2022-02-15 20:57:33 +01:00

693 lines
29 KiB
TeX

\subsection{Grundlagen}
\begin{definition}[Thread]
\vocab[Thread]{Threads} erlauben mehrere execution flows innerhalb eines Prozesses.
Alle Threads werden unabhängig voneinander parallel ausgeführt.
Threads teilen sich einen gemeinsamen Adressraum und können somit auf die selben globalen Variablen zugreifen.
Threads teilen sich:
\begin{itemize}
\item Adressraum
\item Geöffnete Dateien
\item Child-Prozesse
\item Alarme und Signale
\end{itemize}
Jeder Thread besitzt:
\begin{itemize}
\item Stack
\item Program counter
\item Register
\item State (executing, blocked, \dots)
\end{itemize}
\end{definition}
\begin{warning}
Das Betriebssystem schützt Threads des selben Prozesses nicht voreinander. Threads können beispielsweise gegenseitig auf den jeweils anderen Stack zugreifen und diesen manipulieren.
\end{warning}
Threads ermöglichen die parallele Ausführung mehrerer Aufgabe, so kann z.B.~ein Thread blockiert sein, während ein anderer weiterarbeitet. Auf Mehrprozessorsystemen können Threads außerdem verschiedene Kerne nutzen.
\subsubsection{Beispielszenario}
Threads werden häufig eingesetzt, wenn ein Prozess \vocab{responsive} bleiben soll, während eine längere Zeit in Anspruch nehmende Aufgabe ausgeführt wird.
Denkbar ist z.B.~folgende Aufteilung
\begin{itemize}
\item \textbf{User interface thread} kümmert sich um das UI
\item \textbf{Processing thread} erledigt aufwändige Berechnungen (viel CPU-Zeit)
\item \textbf{Communication thread} sendet, empfängt und verarbeitet Nachrichten. Blockiert, während auf externe Ressourcen zugegriffen wird.
\end{itemize}
\subsubsection{Threads vs. Processes}
\begin{minipage}[t]{0.49\textwidth}
\textbf{Threads}
\todoimg{2-2-10}
\begin{itemize}
\item Gemeinsamer Adressraum
\item Schnelle context switches
\item Effiziente Kommunikation und Kooperation
\item Schwieriger zu synchronisieren
\item Ein Fehler in einem Thread betrifft auch alle anderen Threads
\end{itemize}
\end{minipage}
\begin{minipage}[t]{0.49\textwidth}
\textbf{Processes}
\todoimg{2-2-10}
\begin{itemize}
\item Voneinander isoliert
\item Kommunikation und Kooperation sind komplex.
\end{itemize}
\end{minipage}
\subsubsection{OS Support für Threads}
Threading kann entweder auf User level oder auf Kernel level angeboten werden.
\vocab[User Thread]{User Threads} werden von einer Library auf User Level angeboten. Der Kernel weiß nicht, dass es sich um Threads handelt. User Threads können daher schnell erstellt und verwaltet werden, jedoch nur einen Prozessorkern nutzen und ein blockierender system call kann ggf. alle Threads blockieren.
\vocab[Kernel Thread]{Kernel Threads} sind als Teil des OS implementiert. Der Kernel kümmert sich um die Verwaltung und das Scheduling.
Threads auf User level können auf verschiedene Art und Weise auf Kernel Threads abgebildet werden:
\begin{itemize}
\item One-to-One
\begin{itemize}
\item Alle Vor- und Nachteile von Kernel Threads
\item Linux, Solaris, Windows
\end{itemize}
\item Many-to-One
\begin{itemize}
\item Alle Vor- und Nachteile von User Threads
\item Beispiel: GNU Portable Threads
\end{itemize}
\item Many-to-Many
\begin{itemize}
\item Etwas flexibler
\item Ältere Versionen von Solaris, Golang
\end{itemize}
\end{itemize}
\subsection{Threads unter Linux}
Linux unterscheide im Kernel nicht wirklich zwischen Prozessen und Threads. Sie unterscheiden sich nur darin, dass Threads den Adressraum teilen.
Der Begriff \vocab{Task} wird verwendet, um Prozesse oder Threads zu beschreiben.
Neue Threads werden mit \code{clone()} erstellt. Das funktioniert analog zu \code{fork()}. Es kann konfiguriert werden, was genau geteilt wird, z.B. Adressraum, file descriptor table, signal handler, \dots
In älteren Kernelversionen hatte jeder Thread eine eigene pid, dies wurde allerdings mittlerweile geändert. Threads haben eine bzgl. des Prozesses eindeutige Thread ID.
In der Regel wird nicht direkt mit \code{clone()} gearbeitet, sondern mit Bibliotheken, welche intern \code{clone()} einsetzen.
In \code{C11} wurde \code{<threads.h>} eingeführt. Glibc unterstützt ab 2.3 die ``Native POSIX Thread Library''.
POSIX Threads (auch \vocab{PThreads}) sind ein standardisiertes Interface für Threads und für verschiedene Betriebssysteme implementiert. Linux unterstützte ursprünglich LinuxThreads\footnote{deprecated}, seit 2.6 auch \vocab{NPTL} (Native POSIX Threads Library.
Funktionen und Datentypen sind in \code{<pthread.h>} implementiert. Beim Linken muss \textbf{libpthread} mit \code{-lpthread} eingebunden werden.
\subsubsection{PThreads}
\subsubsubsection{Creating PThreads}
\begin{lstlisting}
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void*), void *arg);
// thread - Data structure representing the thread
// attr - Attributes. NULL to use defaults
// start_routine - Pointer to thread function
// arg - Arguments passed to start routine
// returns 0 if ok, error codes otherwise
\end{lstlisting}
\begin{example}
~
\begin{lstlisting}
#include <pthread.h>
#include <stdio.h>
pthread_t workThreadID;
void *workThreadFunction(void *arg) {
// ...
return ((void *) 0);
}
int main() {
pthread_create(&workThreadID, NULL, workThreadFunction, NULL);
sleep(2);
return 0;
}
\end{lstlisting}
\end{example}
\subsubsubsection{PThread Thread Attributes}
\begin{lstlisting}
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
// getters and setters are of the form:
int pthead_attr_setAttrName(pthread_attr_t *attr, AttrType t);
int pthead_attr_getAttrName(pthread_attr_t *attr, AttrType *t);
// detach state (PTHREAD_CREATE_DETACHED or PTHREAD_CREATE_JOINABLE)
int pthread_attr_setdetachstate(pthread_attr_t *attr, int d);
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *d);
// scheduling
// schedpolicy, scope, inheritsched
// stack size and address
// stacksize, stackaddr
\end{lstlisting}
\begin{example}
~
\begin{lstlisting}
int main() {
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&threadID, &attr, threadFunction, NULL);
// attr is no longer relevant after thread was created
pthread_attr_destroy(&attr);
// ...
}
\end{lstlisting}
\end{example}
\subsubsubsection{Parameter Passing}
Argumente werden als \code{void*} übergeben.
\begin{warning}
Die übergebenen Argumente sollten auf dem Heap leben und müssen insbesondere noch existieren, wenn der Thread sie verwendet.
\end{warning}
\subsubsubsection{Threads beenden}
Threads können auf verschiedene Arten beendet werden:
\begin{itemize}
\item Implizit: Wenn die Thread Funktion ausgeführt wurde, ist der Rückgabewert der Funktion Rückgabewert des Threads.
\item Explizit mit \code{pthread_exit}
\begin{lstlisting}
void pthread_exit(void *return_val);
\end{lstlisting}
\item Durch einen anderen Thread des selben Prozesses:
\begin{lstlisting}
int pthread_cancel(pthread_t thread);
\end{lstlisting}
Das ist i.d.R. aber keine gute Lösung.
Ob ein Thread auf diese Weise beendet werden kann wird durch die \vocab[Thread!cancelability]{cancelability} festgelegt:
\begin{lstlisting}
int pthread_setcancelstate(int state, int *oldstate);
// PTHREAD_CANCEL_ENABLE or PTHREAD_CANCEL_DISABLE
\end{lstlisting}
Außerdem ist \vocab[Thread!deferred cancelable]{deferred cancelable} (synchron, \code{PTHREAD_CANCLE_DEFERRED}, default) und \vocab[Thread!asynchronous cancelable]{asynchronous cancelable} ( \code{PTHREAD_CANCEL_ASYNCHRONOUS}) möglich:
\begin{lstlisting}
int pthread_setcanceltype(int type, int *oldtype);
\end{lstlisting}
Ein Thread, welcher synchron cancelable ist, kann nur zu bestimmten cancellation points beendet werden. Diese sind explizite Aufrufe von \code{pthread_testcancel()} oder implizit, z.B. \code{pthread_join}, \code{pthread_cond_wait}, \code{sem_wait}
\end{itemize}
Asynchronous canceling ist in der Regel eine schlecht Idee. Synchronous cancelling ist schwierig, da man auf implizite cancellation points achten muss (insbesondere auch in Subroutinen). Empfehlenswert ist es daher, canceling zu vermeiden und einem Thread explizit, z.B. über eine geteilte Variable, mitzuteilen, dass sich dieser beenden soll.
\subsubsubsection{Joining a Thread}
Der Hauptthread sollte warten bis alle Threads beendet wurden.\footnote{ \code{sleep} ist keine gute Lösung}
Um auf einen Thread zu warten und an dessen return value zu gelangen existiert
\begin{lstlisting}
int pthread_joint(pthread_t thread, void **return_val);
\end{lstlisting}
\begin{example}
\begin{lstlisting}
void *workThreadFunction(void *arg) {
int answer = 42;
return ((void*) answer);
}
int main() {
int result;
pthread_create(&workThreadID, NULL, workThreadFunction, NULL);
pthread_join(workThreadID, (void*) &threadResult);
//...
}
\end{lstlisting}
\end{example}
\begin{warning}
Rückgabewerte von Threads sollte auf dem Heap liegen.
\end{warning}
\subsubsubsection{Detach State}
Ein Thread ist entweder \vocab[Thread!joinable]{joinable} (default) oder \vocab[Thread!detached]{detached}. Dies kann beim Starten spezifiziert werden. Ferner können mit \code{int pthread_detach(pthread_t thread);} Threds detached werden.
Ein joinable Thread existiert, bis explizit \code{pthread_join} aufgerufen wird. So kann ein Ergebnis aus dem Thread gelesen werden.
Ein detached Thread wird automatisch aufgeräumt wenn er terminiert. Es ist nicht möglich, den Rückgabewert auszulesen. Detached Threads sind also nur sinnvoll, wenn sie entweder völlig unabhängig sind, oder ihr Ergebnis anderweitig kommunizieren (z.B. über shared state).
Ein Thread, der weder detached ist noch gejoint wurde, wird nach dem Terminieren zum \vocab{Zombie}.
\subsection{Thread Synchronization}
Es ist nicht garantiert, in welcher Reihenfolge Threads ausgeführt werden. Allerdings sind Threads nicht unabhängig voneinander, Teilen sich die selben Daten, warten auf Ergebnisse anderer Threads, benötigen die selben Ressourcen, \dots
\begin{definition}[Race Condition]
Eine \vocab{Race Condition} liegt vor, wenn das Ergebnis einer Berechnung von der Ausführungsreihenfolge abhängt, z.B. weil auf die selbe Ressource zugegriffen wird, oder eine bestimmte Reihenfolge von Operationen erwartet, aber nicht erzwungen wird.
\end{definition}
\begin{example}
In eine doppelt verkettete Liste fügen mehrere Threads Elemente ein. Dabei kann viel schief gehen.
\end{example}
\begin{definition}[Critical Section / Region]
A \vocab{critical section} is a part of the code that accesses shared resources that must not be concurrently accessed by more than one thread of execution.
\end{definition}
Um gleichzeitigen Zugriff auf critical sections zu verhindert werden synchronization primitives benötigt.
\subsubsection{Mutex variables}
\begin{definition}[Mutex]
Ein \vocab{Mutex} (\textbf{Mut}ual \textbf{Ex}clusion) sorgt dafür, dass nur ein Thread zeitgleich in einen kritischen Bereich für eine bestimmte Ressource eintreten kann.
\textbf{Invariante}: Zu jedem Zeitpunkt besitzt nur ein Thread einen \vocab{Lock} für eine Mutex Variable.
Ein Thread, der ein Lock anfragt, muss ggf. warten, wenn ein anderer Thread dieses bereits hat.
Es gibt keine FIFO Garantie für die Reihenfolge, in der das Lock angefragt wird.
\end{definition}
\subsubsubsection{PThread Mutexes}
\begin{lstlisting}
pthread_mutex_t // mutex variable
pthread_mutexattr_t // mutex attributes
// initialization
// dynamically
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr);
// statically
pthread_mutex_t mutexVar = PTHREAD_MUTEX_INITIALIZER;
// destruction
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// initialization of arguments
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
// destruction
int pthread_mutexattr_destroy(pthread-mutexattr_t *attr);
// setting attributes
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
// lock
int pthread_mutex_lock(pthread_mutex_t *mutex); // blocks
int pthread_mutex_trylock(pthread_mutex_t *mutex); // 0 if locked, EBUSY otherwise
// unlock
int pthread_mutex_unlock(pthread_mutex_t *mutex);
\end{lstlisting}
\paragraph{Mutex Types}
\begin{itemize}
\item \code{PTHREAD_MUTEX_NORMAL}
\begin{itemize}
\item schnell, keine Überprüfungen
\item Deadlock wenn der selbe Thread zweimal ein Lock anfordert
\item Undefined behavior wenn ein Mutex unlocked wird, der nicht gelocked war
\end{itemize}
\item \code{PTHREAD_MUTEX_ERRORCHECK} : Fehler bei relock oder illegalem unlock
\item \code{PTHREAD_MUTEX_RECURSIVE}: Mehrfache Locks des selben Thread sind möglich, es muss entsprechend oft wieder unlocked werden.
\item \code{PTHREAD_MUTEX_DEFAULT}: Hängt von der Implementierung ab
\end{itemize}
\subsubsection{Condition variables}
\begin{definition}[Condition Variable]
\vocab[Condition Variable]{Condition Variables} lösen das Problem, das ein Thread auf Eintreten eines bestimmten Zustands warten möchte.
Condition Variables werden immer zusammen mit einem Mutex benutzt. Um auf die Condition zu warten, wird ein Lock auf den Mutex benötigt. Sobald gewartet wird, wird dieses Lock implizit aufgegeben. Wird die Bedingung erfüllt, so erhält der Thread das Lock zurück und wird fortgesetzt.
Es gibt dabei keinerlei FIFO-Garantien.
\end{definition}
\begin{example}
Ein Consumer Thread wartet auf Daten eines Producer Thread.
\end{example}
Eine schlechte Lösung für diese Problem ist \vocab{Busy Waiting} / \vocab{Spinning}:
\begin{lstlisting}
int localFlag;
while(!localFlag){
pthread_mutex_lock(&flagMutex);
localFlag = flag;
pthread_mutex_unlock(&flagMutex);
}
// Condition fulfilled!
\end{lstlisting}
Hierdurch wird unnötig Rechenleistung verschwendet.
\todoimg{2-2-65}
\subsubsubsection{PThread Condition Variables}
\begin{definition}
Eine \vocab{condition variable} ist immer mit einem mutex assoziiert.
Ein Thread, der das mutex hält, kann auf die condition warten, und gibt
dadurch das mutex auf.
Durch ein Signal auf diese condition variable wird der thread wieder
aufgeweckt, sobald das mutex frei ist, und erhält das mutex wieder.
\end{definition}
\begin{lstlisting}
pthread_cond_t // condition variable data type
pthread-condattr_t // condition variable attributes
//initialization
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
pthread_cond_t condVar = PTHREAD_COND_INITIALIZER;
//destructor
int pthread_cond_destroy(pthread_cond_t *cond);
//initialization and destruction of attributes
int pthread_condattr_init(pthread_cond_attr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthead_mutex_t *mutex,
const struc timespec *abstime);
// waits only until abstime, returns ETIMEDOUT if abstime passed
int pthread_cond_signal(pthread_cond_t *cond);
// unblocks at least one thread (if there are threads waiting)
int pthread_cond_broadcast(pthread_cond_t *cond);
// unblocks all threads
\end{lstlisting}
In der Regel werden keine besonderen Attribute benötigt.
Die Nutzung sieht dann so aus:
\begin{lstlisting}[gobble=2]
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
// Signal has been received
// Do work
pthread_mutex_unlock(&mutex);
\end{lstlisting}
oder
\begin{lstlisting}[gobble=2]
struct timespec tspec;
struct timeval tval;
pthread_mutex_lock(&mutex);
result = gettimeofday(&tval, NULL);
tspec.tv_sec = tval.tv_sec;
tspec.tv_nsec = tval.tv_usec * 1000;
tspec.tv_sec += 5;
if(pthread_cond_timedwait(&cond, &mutex, &tsepc) == ETIMEDOUT) {
/* timeout */
} else { /* signal received */ }
pthread_mutex_unlock(&mutex);
\end{lstlisting}
\begin{warning}
Der Thread kann länger blockiert bleiben als die maximale Wartezeit, da das Lock auf den Mutex benötigt wird.
\end{warning}
% Kehren wir nicht bei |abstime| zurück und erhalten gerade \emph{nicht} das mutex lock? ~ Max
% nein. Wenn der Thread unblocked wird erhalten wir immer das Lock zurück (siehe 2-2 S.71) ~ Josia
\begin{warning}
Spurious wakeups sind möglich, d.h. dass die Condition bereits von einem anderen Thread behandelt wurde und möglicherweise nicht mehr besteht. Daher sollte Folgendes verwendet werden:
\begin{lstlisting}[gobble=4]
pthread_mutex_lock(&threadFlagMutex);
while(!threadFlag) { //also covers the case that condition already exists
pthread_cond_wait(&thredFlagCondition, &threadFlagMutex);
}
pthread_mutex_unlock(&threadFlagMutex);
\end{lstlisting}
\end{warning}
% Wir sagen, dass ein signal 'unblocks at least one thread'.
% Sagen wir, thread B wartet auf eine condition,
% A holt sich jetzt das mutex, gibt ein signal
% Dann kann B noch nicht aufwachen, weil A das mutex hält.
% Bedeutet ein 'signal' dann einfach nur dass
% zum nächsten zeitpunkt, an dem das mutex frei ist,
% einer der wartenden threads aufgeweckt wird und das mutex erhält?
%
% Falls ja, wie verhält sich das dann mit einem \code{pthread_cond_broadcast}?
% Wir können nicht alle threads gleichzeitig aufwecken,
% wegen den mutexes, die sie erhalten müssen.
%
% Bedeutet das umgekehrt nur, dass jeder dieser threads,
% solange das mutex hinreichend oft frei ist,
% irgendwann aufgeweckt wird?
% ~ Max
% ja, exakt ~ Josia
\subsubsection{Semaphores}
Manchmal ist es notwendig, die Anzahl an geleichzeitig auf eine Ressource zugreifenden Threads zu begrenzen.
\begin{definition}[Semaphore]\label{semaphore}
Eine \vocab{Semaphore} ist ein geteilter Counter, der zählt, wie viele Threads auf eine Ressource zugreifen dürfen.
Ein Thread, der zugreifen möchte, wartet bis der Wert $>0$ ist und dekrementiert diesen dann. Nach dem Zugriff wird der Wert wieder inkrementiert.
\end{definition}
\begin{remark}\label{semaphoremutex}
Ein Mutex ist der Spezialfall einer mit $1$ initialisierten Semaphore.
\end{remark}
\begin{warning}
Bei der Verwendung von Semaphoren kann man leicht Fehler machen. Häufig benötigt man Semaphoren nicht. Insbesondere ist \autoref{semaphoremutex} nicht als Anleitung zu verstehen.
\end{warning}
\begin{remark}
Es kann durchaus sinnvoll sein, eine Semaphore mit $0$ zu initialisieren. Damit kann beispielsweise eine Anzahl von zu konsumierenden Objekten verwaltet werden. Ein Produzent inkrementiert dann die Semaphore ohne sie zuvor dekrementiert zu haben.
\end{remark}
\todoimg{2-2 S.78}
\subsubsubsection{PThread Semaphores}
\begin{lstlisting}
#include <semaphore.h>
sem_t // semahore data type
//initialization
int sem_init(sem_t *sem, int pshared, unsigned int value);
// sem - the semaphore
// pshared - indicates whether shared among processes
// value - initial value
// destructor
int sem_destroy(sem_t *sem);
//waiting
int sem_wait(sem_t *sem); // blocks
int sem_trywait(sem_t *sem); // 0 if locked, otherwise EAGAIN
//releasing
int sem_post(sem_t *sem);
\end{lstlisting}
\begin{remark}
\code{sem_wait(sem_t *sem)} dekrementiert den wert der Semaphore,
sobald der aufruf returned.
\code{sem_post(sem_t *sem)} inkrementiert den Wert.
Der Aufruf ist auch legal, wenn man davor nicht \code{sem_wait} aufgerufen hat,
der Wert wird wie üblich inkrementiert.
Dadurch kann man z.B.~verwalten, wie viele Ressourcen man bearbeiten muss.
Ein producer thread kann dann die semaphore inkrementieren.
\end{remark}
\subsubsection{Barriers}
Threads müssen manchmal aufeinander warten. Erst wenn alle Threads einen bestimmten Punkt erreicht haben, soll die Ausführung fortgesetzt werden. Hierfür eignen sich Barriers:
\begin{definition}[Barrier]
Eine \vocab{Barrier} zählt die Anzahl an Threads, die an einer bestimmten Stelle warten. Sie wird mit der Zahl der beteiligten Threads initialisiert.
Jeder Thread, der die \vocab[Barrier!wait operation]{wait operation} ausführt, inkrementiert einen Zähler. Falls \code{counter < nBarr} blockiert der Thread. Falls \code{counter == nBarr} werden alle Threads unblocked und der Counter zurück auf $0$ gesetzt.
\end{definition}
\todoimg{2-2 S. 85}
\subsubsubsection{PThread Barriers}
\begin{lstlisting}
pthread_barrier_t // barrier data type
pthread_barrierattr_t
// initialization
int pthread_barrier_init(pthread_barrier_t *restrict barrier, pthread_barrierattr_t *restrict attr, unsigned count);
int pthread_barrierattr_init(pthread_barrierattr_t *attr);
// destructor
int pthread_barrier_destroy(pthread_barrier_t *barrier);
int pthread_barrierattr_destroy(pthread_barrierattr_t *attr);
// getter and setter
int pthread_barrierattr_getpshared(const pthread_barrierattr_t *restrict attr, int *restrict shared);
int pthread_barrierattr_setpshared(pthread_barrierattr_t *attr, int shared);
// waiting
int pthread_parrier_wait(pthread_barrier_t *);
\end{lstlisting}
\begin{example}
~
\begin{lstlisting}
void* workThreadFunction(void* args) {
// do work
pthread_barrier_wait(&threadBarrier);
// do more work
}
void init() {
pthread_barrier_init(&threadBarrier, NULL, 5);
for(int i = 0; i < 5; ++i){
pthread-create(&threadID[i], NULL, workThreadFunction, NULL);
}
// ...
}
\end{lstlisting}
\end{example}
\subsubsection{Read-write locks}
\begin{idea}
Wenn nur gelesen und nichts geändert wird, kann das problemlos gleichzeitig passieren. Daher macht es Sinn, lesenden und schreibenden Zugriff zu unterscheiden.
\end{idea}
\begin{definition}[Read-Write Lock]
Ein \vocab{Read-Write Lock} stellt sicher, dass zu jedem Zeitpunkt eine der folgenden Bedingungen gilt:
\begin{itemize}
\item Ein Thread hat Schreibrechte, kein anderer Thread hat Leserechte
\item Kein Thread hat Schreibrechte, beliebig viele Threads haben Leserechte
\end{itemize}
Wird ein Lesezugriff angefragt, so wird blockiert, falls aktuell ein Schreibzugriff vergeben wurde.
Wird ein Schreibzugriff angefragt, so wird blockiert bis alle anderen Locks aufgegeben wurden.
\end{definition}
\todoimg{2-2 S.90}
\subsubsubsection{PThread Read-Write Lock}
\begin{lstlisting}
pthread_rwlock_t // rw lock data type
pthread_rwlockattr_t // rw lock attributes data type
//initialization
int pthread_rwlock-init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// destructor
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//acquiring read lock
int pthread_rwlock_rdlock(pthread_rwlock_t * rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t * rwlock);
//acquiring write lock
int pthread_rwlock_wrlock(pthread_rwlock_t * rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t * rwlock);
//releasing a lock (read or write)
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
\end{lstlisting}
\subsubsection{Channels}
\begin{definition}[Channel]
Ein \vocab{Channel} ermöglicht die Kommunikation zwischen Threads ohne dass sich der Programmierer selbst um die Synchronization kümmern muss. In einen Channel wird von einer Seite geschrieben und von der anderen Seite gelesen werden. Bei einem vollen Puffer blockiert der Channel.
\end{definition}
\subsection{Deadlocks}
\begin{definition}
Ein \vocab{Deadlock} ist eine Situation, in der mindestens zwei Threads (oder Prozesse) aufeinander warten, aber dabei den jeweils anderen Thread blockieren.
\end{definition}
\todoimg{2-2 S.98}
Damit ein Deadlock entstehen kann müssen alle der folgenden \vocab{Deadlock Conditions} erfüllt sein:
\begin{itemize}
\item \textbf{Mutual exclusion}: Ressourcen können nur von einem Thread zur Zeit verwendet werden.
\item \textbf{Non preemption}: Ressourcen werden nur freiwillig aufgegeben und können nicht gewaltsam entzogen werden.
\item \textbf{Hold \& wait}: Threads können Ressourcen besitzen, während sie auf andere Threads warten.
\item \textbf{Circular wait}: Es existiert ein gerichteter Kreis von Threads, die auf eine Ressource ihres jeweiligen Nachbarn warten.
\end{itemize}
Die ersten drei Bedingungen sind durch Synchronization gegeben.
\begin{definition}[Ressource-Allocation Graph]
Ein \vocab{Ressource-Allocation Graph} ist ein bipartiter Digraph, dessen Knoten Prozesse (Kreise) und Ressourcen (Rechtecke) sind.
Der Wait-For Graph enthält die Kanten $(p,r)$, für die der Prozess $p$ auf die Ressource $r$ wartet, und die Kanten $(r,p)$, für die die Ressource $r$ dem Prozess $p$ gehört.
Alle Ressourcen haben folglich Augangsgrad $\le 1$.
\end{definition}
\begin{definition}[Process Wait-For Graph]
Ein \vocab{Process Wait-For Graph} geht aus einem Ressource-Allocation Graph hervor, indem die alle Kanten $(r,p)$ von Ressourcen zu Prozessen kontrahiert und alle Ressourcen mit Ausgangsgrad $0$ entfernt werden.
\end{definition}
\begin{observe}
Ein gerichteter Kreis im Ressource-Allocation Graph oder Process Wait-For Graph enspricht der Deadlock Condition des Circular Wait.
\end{observe}
Es gibt verschiedene Möglichkeiten, mit Deadlocks umzugehen:
\begin{itemize}
\item \textbf{Deadlock prevention}: Stelle sicher, dass eine der Deadlock Conditions nie eintritt.
Es existieren mehrere Ansätze:
\begin{itemize}
\item Jeder Thread darf nur eine Ressource zur Zeit benutzen (kein Hold \& wait)
\item Wenn ein Thread wartet werden alle Ressourcen entzogen (keine Preemption condition)
\item Totalordnung auf den Ressourcen angeben, Ressourcen dürfen nur in dieser Reihenfolge angefragt werden (kein Circular wait)
\end{itemize}
All dass kann sehr teuer sein!
\item \textbf{Deadlock avoidance}: Ein Supervisor teilt die Ressourcen zu und besitzt dafür Informatione darüber, welcher Thread welche Ressourcen anfragen wird.
\item \textbf{Deadlock detection}: Deadlocks können auftreten. Es wird regelmäßig überprüft, ob ein Deadlock besteht und ggf. ein Prozess beendet.
\item \textbf{Suspecting deadlocks}: Prozesse werden beendet, wenn der Verdacht besteht, dass diese ein Deadlock verursachen.
\end{itemize}
\subsection{Wichtige Threading Mechanismen}
\subsubsection{Worker Threads}
Programme müssen häufig lange laufende Aufgaben erledigen. Das Programm soll dabei weiterhin auf Eingaben oder andere Ereignisse reagieren können.
\begin{definition}[Worker Threads]
Ein \vocab{Dispatcher}-Thread empfängt alle zu erledigenden Aufgaben. Der Dispatcher erstellt für jede Aufgabe einen \vocab{Worker Thread}. Wenn ein Worker Thread fertig ist, meldet dieser das Ergebnis an den Dispatcher.
\end{definition}
Diese Modell wird häufig für Webserver verwendet, in diesem Fall melden die Worker Threads sich nicht beim Dispatcher zurück, sondern senden die Ergebnisse direkt an den Client.
Dieser Mechanismus ist konzeptuell sehr einfach. Wenn sehr viele Tasks verwendet werden, kann Synchronization, z.B. für den Zugriff auf Daten oder das Zurückmelden von Ergebnissen, jedoch aufwändig werden.
\todoimg{2-2}
\subsubsection{Thread Pools}
\begin{definition}[Thread Pool]
\vocab[Thread Pool]{Thread Pools} werden eingesetzt, wenn viele unabhängige, häufig kurzlebige Aufgaben erledigt werden müssen. Um den Overhead durch Threaderstellung zu minimieren, wird eine feste Anzahl an Threads (der Thread Pool) zu Beginn gestartet. Diese arbeiten Aufgaben aus einer \vocab{Task Queue} ab. Existieren dort keine Aufgaben, so blockieren Threads, die nichts zu tun haben. Sobald weitere Aufgaben anfallen werden diese Threads benachrichtigt.
\end{definition}
\todoimg{2-2 111}
Der Zugriff auf die request queue muss synchronisiert werden.
Resultate können ggf. in eine weitere Queue geschreiben werden.
Als Variante ist es möglich, die Zahl der Threads im Pool dynamisch anzupassen. Außerdem kann statt einer queue eine priority queue für die anfallenden Aufgaben eingesetzt werden.
\subsubsection{Thread-Safe Libraries}
\begin{definition}[Thread safe]
Code oder Bibliotheken heißen \vocab{thread safe}, wenn diese parallel von mehreren Threads verwendet werden können.
\end{definition}
Es ist wichtig, ein der Bibliothek nachzulesen, ob ein Aufruf tatsächlich thread safe ist. In manchen Fällen sind spezielle wiedereintretbare Versionen von Funktionen verfügbar.
Für die Threadsicherheit muss Folgendes sicher gestellt werden:
\begin{itemize}
\item \vocab{Wiedereintrittsfähigkeit} (Reentrancy) von Funktionen:
Eine Funktion kann unterbrochen und während der Unterbrechung neu aufgerufen werden. Insbesondere darf dafürnur auf \textbf{purely local state} zugegriffen werden.
\item \vocab{Mutual exclusion}: Zugriff auf critical sections muss geschützt werden.
\end{itemize}
Folgendes ist dafür hilfreich:
\begin{itemize}
\item \textbf{Thread-local data}: Daten, die nicht mit anderen Threads geteilt werden sollen, werden für jeden Thread kopiert.
\item \textbf{Atomare Operationen für den Zugriff auf geteilte Daten}
\item \textbf{Speicher mittels Kommunikation teilen}
\end{itemize}
\begin{warning}
Thread-safety sorgt in der Regel für Overhead und wird daher nicht überall unterstützt.
\end{warning}