diff --git a/.gitignore b/.gitignore index 3d8789c..e5e3700 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,15 @@ dkms.conf # debug information files *.dwo +# LaTeX +.auctex-auto/ +*.fls +*.log +*.synctex.gz +*.fdb_latexmk +*.aux +*.pdf + # Nix related things .direnv/ result*/ diff --git a/flake.nix b/flake.nix index 03d2962..dd5848a 100644 --- a/flake.nix +++ b/flake.nix @@ -11,6 +11,7 @@ subdirs = [ "assignment1" "assignment2" + "project1" ]; eachSystem = nixpkgs.lib.genAttrs (import systems); diff --git a/project1/README.md b/project1/README.md new file mode 100644 index 0000000..afe523a --- /dev/null +++ b/project1/README.md @@ -0,0 +1,25 @@ +# CS3502 Project 1: Asynchronous Banking Account Management + +This directory contains multiple toy models of asynchronous bank account systems, demonstrating the principles of race condition and deadlock avoidance. + +## Usage + +There are 4 programs labeled Phases 1-4: + +- **Phase 1**: Naive non-supervised multi-threaded account system +- **Phase 2**: Mutex-based account system +- **Phase 3**: Deadlock generation +- **Phase 4**: Deadlock resolution via lock ordering + +If you have [Nix](https://nixos.org/) installed: + +```sh +nix run .#p1-phase1 +``` + +Otherwise, the code must be compiled manually: + +``` sh +gcc -c phase1/phase1.c -o phase1.bin +``` + diff --git a/project1/default.nix b/project1/default.nix new file mode 100644 index 0000000..dffb073 --- /dev/null +++ b/project1/default.nix @@ -0,0 +1,23 @@ +let + basicC = subdir: name: { stdenv }: + stdenv.mkDerivation { + inherit name; + src = ./.; + buildPhase = '' + runHook preBuild + gcc ${subdir}/${name}.c -o ${name}.bin + runHook postBuild + ''; + installPhase = '' + runHook preInstall + mkdir -p $out/bin + install ${name}.bin $out/bin/${name} + runHook postInstall + ''; + }; +in { + p1-phase1 = basicC "phase1" "phase1"; + p1-phase2 = basicC "phase2" "phase2"; + p1-phase3 = basicC "phase3" "phase3"; + p1-phase4 = basicC "phase4" "phase4"; +} diff --git a/project1/phase1/output.png b/project1/phase1/output.png new file mode 100644 index 0000000..e2f0d62 Binary files /dev/null and b/project1/phase1/output.png differ diff --git a/project1/phase1/phase1.c b/project1/phase1/phase1.c new file mode 100644 index 0000000..d49d859 --- /dev/null +++ b/project1/phase1/phase1.c @@ -0,0 +1,107 @@ +#include +#include +#include +#include +#include +#include +#include + +#define NUM_ACCOUNTS 8 +#define NUM_TELLERS 128 +#define TRANSACTIONS_PER_TELLER 100 + +// Monetary value in cents +typedef long currency; + +void printf_currency(currency c) { + if (c < 0) { + c = -c; + printf("-"); + } + printf("$%ld.%.2ld", c / 100, c % 100); +} + +// Accounts (shared resource) +typedef struct account { + int account_id; + currency balance; + int transaction_count; +} account; + +// Global account array +account accounts[NUM_ACCOUNTS]; + +// Teller threads + +pthread_t tellers[NUM_TELLERS]; + +typedef struct teller_args { + int teller_id; + currency total; +} teller_args; + +void *teller_thread(void *arg) { + teller_args *args = (teller_args *)arg; + int id = args->teller_id; + currency total = 0; + + unsigned int seed = time(NULL) + pthread_self(); + for (int i = 0; i < TRANSACTIONS_PER_TELLER; i++) { + printf("Thread %u: ", id); + // Deposit or withdraw random currency amount from $0 to $1000 + currency random_amount = ((long)rand_r(&seed)) % 100000; + if (rand_r(&seed) % 2) { + printf("Withdrawing "); + printf_currency(random_amount); + random_amount = -random_amount; + } else { + printf("Depositing "); + printf_currency(random_amount); + } + + int random_acc = rand_r(&seed) % NUM_ACCOUNTS; + accounts[random_acc].balance += random_amount; + accounts[random_acc].transaction_count++; + + printf(" into account %u\n", random_acc); + + total += random_amount; + } + + args->total = total; + return NULL; +} + +int main() { + // Initialize all accounts with 0 balance + for (int i = 0; i < NUM_ACCOUNTS; i++) { + accounts[i] = (account){.account_id = i, .balance = 0}; + } + + // Initialize tellers + teller_args args[NUM_TELLERS]; + for (int i = 0; i < NUM_TELLERS; i++) { + args[i] = (teller_args){.teller_id = i}; + pthread_create(&tellers[i], NULL, teller_thread, &args[i]); + } + + // Wait for all threads to finish + currency expected_total = 0; + for (int i = 0; i < NUM_TELLERS; i++) { + pthread_join(tellers[i], NULL); + expected_total += args[i].total; + } + + currency actual_total = 0; + for (int i = 0; i < NUM_ACCOUNTS; i++) { + actual_total += accounts[i].balance; + } + + printf("Total balance: "); + printf_currency(actual_total); + printf("\nExpected total: "); + printf_currency(expected_total); + printf("\n"); + + return 0; +} diff --git a/project1/phase2/output.png b/project1/phase2/output.png new file mode 100644 index 0000000..0d67ee1 Binary files /dev/null and b/project1/phase2/output.png differ diff --git a/project1/phase2/phase2.c b/project1/phase2/phase2.c new file mode 100644 index 0000000..9c4911c --- /dev/null +++ b/project1/phase2/phase2.c @@ -0,0 +1,130 @@ +#include +#include +#include +#include +#include +#include +#include + +#define NUM_ACCOUNTS 8 +#define NUM_TELLERS 128 +#define TRANSACTIONS_PER_TELLER 1000 + +// Monetary value in cents +typedef long currency; + +void sprintf_currency(char *buffer, currency c) { + if (c < 0) { + c = -c; + sprintf(buffer, "-"); + buffer += sizeof(char); + } + sprintf(buffer, "$%ld.%.2ld", c / 100, c % 100); +} + +void printf_currency(currency c) { + if (c < 0) { + c = -c; + printf("-"); + } + printf("$%ld.%.2ld", c / 100, c % 100); +} + +// Accounts (shared resource) +typedef struct account { + int account_id; + currency balance; + int transaction_count; + pthread_mutex_t lock; // Account access mutex +} account; + +void deposit(account *acc, currency amount) { + pthread_mutex_lock(&acc->lock); + // CRITICAL SECTION BEGIN + acc->balance += amount; + acc->transaction_count++; + // CRITICAL SECTION END + pthread_mutex_unlock(&acc->lock); +} + +// Global account array +account accounts[NUM_ACCOUNTS]; + +// Teller threads + +pthread_t tellers[NUM_TELLERS]; + +typedef struct teller_args { + int teller_id; + currency total; +} teller_args; + +void *teller_thread(void *arg) { + teller_args *args = (teller_args *)arg; + int id = args->teller_id; + currency total = 0; + + unsigned int seed = time(NULL) + pthread_self(); + for (int i = 0; i < TRANSACTIONS_PER_TELLER; i++) { + // Deposit or withdraw random currency amount from $0 to $1000 + currency random_amount = ((long)rand_r(&seed)) % 100000; + + char *operation; + currency random_amount_i = random_amount; + if (rand_r(&seed) % 2) { + random_amount_i = -random_amount; + operation = "Withdrawing"; + } else { + operation = "Depositing"; + } + + int random_acc = rand_r(&seed) % NUM_ACCOUNTS; + deposit(&accounts[random_acc], random_amount_i); + + // Display result of transaction + char buffer[8]; + sprintf_currency(buffer, random_amount); + printf("Thread %u: %s %s into account %u\n", id, operation, buffer, + random_acc); + + total += random_amount_i; + } + + args->total = total; + return NULL; +} + +int main() { + // Initialize all accounts with 0 balance + for (int i = 0; i < NUM_ACCOUNTS; i++) { + accounts[i] = (account){.account_id = i, .balance = 0}; + pthread_mutex_init(&accounts[i].lock, NULL); + } + + // Initialize tellers + teller_args args[NUM_TELLERS]; + for (int i = 0; i < NUM_TELLERS; i++) { + args[i] = (teller_args){.teller_id = i}; + pthread_create(&tellers[i], NULL, teller_thread, &args[i]); + } + + // Wait for all threads to finish + currency expected_total = 0; + for (int i = 0; i < NUM_TELLERS; i++) { + pthread_join(tellers[i], NULL); + expected_total += args[i].total; + } + + currency actual_total = 0; + for (int i = 0; i < NUM_ACCOUNTS; i++) { + actual_total += accounts[i].balance; + } + + printf("Total balance: "); + printf_currency(actual_total); + printf("\nExpected total: "); + printf_currency(expected_total); + printf("\n"); + + return 0; +} diff --git a/project1/phase3/output.png b/project1/phase3/output.png new file mode 100644 index 0000000..41e1515 Binary files /dev/null and b/project1/phase3/output.png differ diff --git a/project1/phase3/phase3.c b/project1/phase3/phase3.c new file mode 100644 index 0000000..27862a3 --- /dev/null +++ b/project1/phase3/phase3.c @@ -0,0 +1,105 @@ +#include +#include +#include +#include +#include +#include +#include + +#define NUM_ACCOUNTS 5 +#define NUM_TRANSFERS 5 + +// Monetary value in cents +typedef long currency; + +void sprintf_currency(char *buffer, currency c) { + if (c < 0) { + c = -c; + sprintf(buffer, "-"); + buffer += sizeof(char); + } + sprintf(buffer, "$%ld.%.2ld", c / 100, c % 100); +} + +void printf_currency(currency c) { + if (c < 0) { + c = -c; + printf("-"); + } + printf("$%ld.%.2ld", c / 100, c % 100); +} + +// Accounts (shared resource) +typedef struct account { + int account_id; + currency balance; + int transaction_count; + pthread_mutex_t lock; // Account access mutex +} account; + +// Global account array +account accounts[NUM_ACCOUNTS]; + +// Threads + +pthread_t transfers[NUM_TRANSFERS]; + +typedef struct transfer_args { + int transfer_id; + account *from; + account *to; + currency amount; +} transfer_args; + +void *transfer_thread(void *arg) { + transfer_args *args = (transfer_args *)arg; + int id = args->transfer_id; + account *from = args->from; + account *to = args->to; + currency amount = args->amount; + + pthread_mutex_lock(&from->lock); + printf("Thread %u: Locked account %d\n", id, from->account_id); + + usleep(100); // Delay to account for other threads + + pthread_mutex_lock(&to->lock); + printf("Thread %u: Locked account %d\n", id, to->account_id); + + from->balance -= amount; + to->balance += amount; + from->transaction_count++; + to->transaction_count++; + + printf("Thread %u: Unlocked account %u\n", id, from->account_id); + printf("Thread %u: Unlocked account %u\n", id, to->account_id); + pthread_mutex_unlock(&to->lock); + pthread_mutex_unlock(&from->lock); + + return NULL; +} + +int main() { + // Initialize all accounts with 0 balance + for (int i = 0; i < NUM_ACCOUNTS; i++) { + accounts[i] = (account){.account_id = i, .balance = 0}; + pthread_mutex_init(&accounts[i].lock, NULL); + } + + // Initialize transfers + transfer_args args[NUM_TRANSFERS]; + for (int i = 0; i < NUM_TRANSFERS; i++) { + args[i] = (transfer_args){.transfer_id = i, + .from = &accounts[i % NUM_ACCOUNTS], + .to = &accounts[(i + 1) % NUM_ACCOUNTS], + .amount = 1000}; + pthread_create(&transfers[i], NULL, transfer_thread, &args[i]); + } + + // Wait for all threads to finish + for (int i = 0; i < NUM_TRANSFERS; i++) { + pthread_join(transfers[i], NULL); + } + + return 0; +} diff --git a/project1/phase4/mistake.png b/project1/phase4/mistake.png new file mode 100644 index 0000000..860c850 Binary files /dev/null and b/project1/phase4/mistake.png differ diff --git a/project1/phase4/output.png b/project1/phase4/output.png new file mode 100644 index 0000000..5e346b4 Binary files /dev/null and b/project1/phase4/output.png differ diff --git a/project1/phase4/output_wrong.png b/project1/phase4/output_wrong.png new file mode 100644 index 0000000..01574a4 Binary files /dev/null and b/project1/phase4/output_wrong.png differ diff --git a/project1/phase4/phase4.c b/project1/phase4/phase4.c new file mode 100644 index 0000000..bc1c38d --- /dev/null +++ b/project1/phase4/phase4.c @@ -0,0 +1,124 @@ +#include "bits/time.h" +#include +#include +#include +#include +#include +#include +#include + +#define NUM_ACCOUNTS 5 +#define NUM_TRANSFERS 20 + +// Monetary value in cents +typedef long currency; + +void sprintf_currency(char *buffer, currency c) { + if (c < 0) { + c = -c; + sprintf(buffer, "-"); + buffer += sizeof(char); + } + sprintf(buffer, "$%ld.%.2ld", c / 100, c % 100); +} + +void printf_currency(currency c) { + if (c < 0) { + c = -c; + printf("-"); + } + printf("$%ld.%.2ld", c / 100, c % 100); +} + +// Accounts (shared resource) +typedef struct account { + int account_id; + currency balance; + int transaction_count; + pthread_mutex_t lock; // Account access mutex +} account; + +// Global account array +account accounts[NUM_ACCOUNTS]; + +// Threads + +pthread_t transfers[NUM_TRANSFERS]; + +typedef struct transfer_args { + int transfer_id; + account *from; + account *to; + currency amount; +} transfer_args; + +void *transfer_thread(void *arg) { + transfer_args *args = (transfer_args *)arg; + int id = args->transfer_id; + account *from = args->from; + account *to = args->to; + currency amount = args->amount; + + // Sort in increasing order + account *acc1; + account *acc2; + if (from->account_id < to->account_id) { + acc1 = from; + acc2 = to; + } else { + acc1 = to; + acc2 = from; + } + + pthread_mutex_lock(&acc1->lock); + printf("Thread %u: Locked account %u (1)\n", id, acc1->account_id); + + usleep(100); // Delay to account for other threads + + pthread_mutex_lock(&acc2->lock); + printf("Thread %u: Locked account %u (2)\n", id, acc2->account_id); + + from->balance -= amount; + to->balance += amount; + from->transaction_count++; + to->transaction_count++; + + printf("Thread %u: Unlocked account %u\n", id, acc1->account_id); + printf("Thread %u: Unlocked account %u\n", id, acc2->account_id); + pthread_mutex_unlock(&acc2->lock); + pthread_mutex_unlock(&acc1->lock); + + return NULL; +} + +int main() { + // Initialize all accounts with 0 balance + for (int i = 0; i < NUM_ACCOUNTS; i++) { + accounts[i] = (account){.account_id = i, .balance = 0}; + pthread_mutex_init(&accounts[i].lock, NULL); + } + + // Initialize tellers + transfer_args args[NUM_TRANSFERS]; + for (int i = 0; i < NUM_TRANSFERS; i++) { + args[i] = (transfer_args){.transfer_id = i, + .from = &accounts[i % NUM_ACCOUNTS], + .to = &accounts[(i + 1) % NUM_ACCOUNTS], + .amount = 1000}; + pthread_create(&transfers[i], NULL, transfer_thread, &args[i]); + } + + // Wait for all threads to finish + for (int i = 0; i < NUM_TRANSFERS; i++) { + pthread_join(transfers[i], NULL); + } + + // Print account balance totals + for (int i = 0; i < NUM_ACCOUNTS; i++) { + printf("Total in account %u: ", i); + printf_currency(accounts[i].balance); + printf("\n"); + } + + return 0; +} diff --git a/project1/report/report.tex b/project1/report/report.tex new file mode 100644 index 0000000..fc5a935 --- /dev/null +++ b/project1/report/report.tex @@ -0,0 +1,177 @@ +\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} diff --git a/project1/test.nix b/project1/test.nix new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/project1/test.nix @@ -0,0 +1 @@ +{}