\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}