177 lines
7.7 KiB
TeX
177 lines
7.7 KiB
TeX
\documentclass{article}
|
|
|
|
\usepackage{graphicx}
|
|
\usepackage{hyperref}
|
|
|
|
\title{CS3502 Project 1: Resource Management and Deadlocks}
|
|
\author{Kiana Sheibani}
|
|
|
|
\begin{document}
|
|
\maketitle
|
|
\newpage
|
|
|
|
\section{Introduction}
|
|
|
|
To demonstrate the basic principles of multi-threaded code, race conditions, and deadlock
|
|
avoidance, I ultimately decided to go with the model of a banking system, as that serves
|
|
as a simple and immediately applicable demonstration of the concept.
|
|
|
|
The code for this project is stored in the \texttt{project1} subdirectory of my repository
|
|
for this class: \url{https://github.com/tokinanpa/CS3502}.
|
|
|
|
\section{Phase 1: Basic Thread Operations}
|
|
|
|
\subsection{Implementation}
|
|
|
|
To start with, I simply needed some basic code of multiple threads trying to access and
|
|
manipulate an array of bank accounts. This code mostly looked similar to the example code
|
|
given in the instructions, except for the following differences:
|
|
|
|
\begin{enumerate}
|
|
\item The currency amounts are stored as an integer number of cents instead of
|
|
floating point values, because that's the sensible thing to do.
|
|
\item Each thread is passed an argument struct that contains its own ID and a slot
|
|
for it to pass back the total amount it deposited or withdrew.
|
|
\end{enumerate}
|
|
|
|
The program initializes an array of accounts (8) and of teller threads (128), then each
|
|
thread starts depositing or withdrawing a random amount of money up to \$1000 a certain
|
|
amount of times (100), while keeping a count of the total monetary influx that thread
|
|
introduced. After all threads have joined back up with the main program, the combined sum
|
|
from each thread (the \emph{expected total}) is compared with the sum of all account
|
|
balances (the \emph{actual total}). If no race conditions have occurred, these values
|
|
should be equal.
|
|
|
|
\subsection{Results}
|
|
|
|
\begin{figure}[h!]
|
|
\centering
|
|
\includegraphics[width=0.5\linewidth]{../phase1/output.png}
|
|
\caption{The output log of the Phase 1 program.}
|
|
\label{fig:result1}
|
|
\end{figure}
|
|
|
|
As expected, the program did indeed demonstrate that race conditions had occurred between
|
|
the threads, as can be seen in \figurename~\ref{fig:result1}. You may notice that the
|
|
transaction logs are somewhat garbled; this is because the logs are generated through
|
|
multiple \texttt{printf} calls, resulting in another example of a race condition. I did
|
|
not yet fix this bug because the repeated I/O calls helped delay the threads' execution
|
|
and make race conditions more likely.
|
|
|
|
\section{Phase 2: Resource Protection}
|
|
|
|
\subsection{Implementation}
|
|
|
|
This was a very small addition to the code for Phase 1, and just required adding a mutex
|
|
field \texttt{lock} to the account struct and initializing it at the beginning of the main
|
|
program. Each thread had to lock the mutex before the transaction and unlock it after,
|
|
which could be easily encapsulated into a subroutine \texttt{deposit} that takes a pointer
|
|
to the account and an amount.
|
|
|
|
I also fixed the bug shown earlier that garbled the log output by combining the log into
|
|
one \texttt{printf} call, which required a bit of rearranging in my code for printing currency
|
|
values but was only a minor complication.
|
|
|
|
\subsection{Results}
|
|
|
|
\begin{figure}[h!]
|
|
\centering
|
|
\includegraphics[width=0.5\linewidth]{../phase2/output.png}
|
|
\caption{The output of Phase 2, showing total consistency.}
|
|
\label{fig:result2}
|
|
\end{figure}
|
|
|
|
Even after increasing the number of transactions per teller from 100 to 1,000, the totals
|
|
always match up exactly.
|
|
|
|
Notably, there seems to be large blocks of transactions coming from singular threads. This
|
|
happened in the first program as well, most likely as an artifact of scheduling, but the
|
|
blocks are much larger here. This may be due to threads repeatedly unlocking and
|
|
re-locking a mutex, thus allowing them to instantly make another transaction.
|
|
|
|
\section{Phase 3: Deadlock Creation}
|
|
|
|
\subsection{Implementation}
|
|
|
|
It turns out that it's quite easy to make a program deadlock! I could have just had two
|
|
transactions competing for resources as in the example code, but I decided to implement a
|
|
more complex example. I kept the large array of threads, but changed the threads from
|
|
adding/subtracting random amounts to transferring money between two accounts. The accounts
|
|
to transfer between were passed as pointers to the threads using the struct-passing method
|
|
I outlined earlier, along with the amount to transfer (I arbitrarily picked \$10 for all threads).
|
|
|
|
I then initialized the same number of threads as accounts, and set each of them to start a
|
|
transfer from one account to the next. Thread 0 transfers from 0 to 1, thread 1 transfers
|
|
from 1 to 2, etc. This would in theory create a large circle of threads that would
|
|
deadlock and prevent any transaction from completing.
|
|
|
|
\subsection{Results}
|
|
|
|
\begin{figure}[h!]
|
|
\centering
|
|
\includegraphics[width=0.5\linewidth]{../phase3/output.png}
|
|
\caption{The output of Phase 3 successfully generating a deadlock.}
|
|
\label{fig:result3}
|
|
\end{figure}
|
|
|
|
The program hangs immediately once all threads have started, regardless of the number of
|
|
accounts or transfers I generate.
|
|
|
|
\section{Phase 4: Deadlock Resolution}
|
|
|
|
\subsection{Implementation}
|
|
|
|
To resolve the issue of these deadlocks in this code, I ultimately decided to go with a
|
|
lock ordering approach. This is the simplest and easiest to implement of the possible
|
|
solutions, though no program involving asynchonous execution is truly simple. The idea is
|
|
to always lock the resources in a consistent, absolute order (in this case, the order of
|
|
the account IDs). This would prevent the threads from creating a cycle of lock
|
|
acquisitions, and thereby prevent any deadlocks from occurring.
|
|
|
|
\subsubsection{Challenges}
|
|
|
|
\begin{figure}[h!]
|
|
\centering
|
|
\includegraphics[width=0.5\linewidth]{../phase4/output_wrong.png}
|
|
\caption{This log output shows account 0 being locked multiple times!}
|
|
\label{fig:outputwrong}
|
|
\end{figure}
|
|
|
|
After running the code with different parameters, in particular in the case of there being
|
|
more transfers than accounts, I ran into a strange bug. It seemed like the mutexes were
|
|
being locked multiple times at once, which should be impossible. The final balances of
|
|
each account seemed to confirm that race conditions were occurring, as the total did not
|
|
sum to 0, as should be the case when no transfer is adding or removing money from the
|
|
bank.
|
|
|
|
\begin{figure}[h!]
|
|
\centering
|
|
\includegraphics[width=0.5\linewidth]{../phase4/mistake.png}
|
|
\caption{The offending line of code (line 105).}
|
|
\label{fig:mistake}
|
|
\end{figure}
|
|
|
|
It took me a long time to discover the true problem: transactions were receiving invalid
|
|
account IDs because one specific line of code accidentally left off a modulo operator
|
|
(\texttt{i \% NUM\_ACCOUNTS}) to constrain the accounts referenced to inside the array.
|
|
This meant that ``accounts'' were modified that did not actually exist, thus violating the
|
|
preservation of the total sum of balances. The log output issue was completely unrelated
|
|
and seems to solely be an artifact of race conditions in I/O operations. After fixing the
|
|
aforementioned line of code, the final account balances were fully consistent.
|
|
|
|
\subsection{Results}
|
|
|
|
\begin{figure}[h!]
|
|
\centering
|
|
\includegraphics[width=0.5\linewidth]{../phase4/output.png}
|
|
\caption{The output log of Phase 4 showing consistent account balances.}
|
|
\label{fig:result4}
|
|
\end{figure}
|
|
|
|
\figurename~\ref{fig:result4} shows that deadlocks have been completely avoided and that
|
|
no race conditions have occurred. This output was generated with 5 accounts and 20
|
|
transfers, meaning that every account has multiple threads attempting to modify its
|
|
balance without issue. The performance is also unchanged from previous phases, which may
|
|
not be the case for the other deadlock resolution methods (e.g. timed locks).
|
|
|
|
\end{document}
|