feat: project 1

This commit is contained in:
Kiana Sheibani 2025-10-09 22:02:18 -04:00
parent d6ef47cff4
commit f04d70f615
Signed by: toki
GPG key ID: 6CB106C25E86A9F7
16 changed files with 702 additions and 0 deletions

9
.gitignore vendored
View file

@ -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*/

View file

@ -11,6 +11,7 @@
subdirs = [
"assignment1"
"assignment2"
"project1"
];
eachSystem = nixpkgs.lib.genAttrs (import systems);

25
project1/README.md Normal file
View file

@ -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
```

23
project1/default.nix Normal file
View file

@ -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";
}

BIN
project1/phase1/output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

107
project1/phase1/phase1.c Normal file
View file

@ -0,0 +1,107 @@
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#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;
}

BIN
project1/phase2/output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

130
project1/phase2/phase2.c Normal file
View file

@ -0,0 +1,130 @@
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#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;
}

BIN
project1/phase3/output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

105
project1/phase3/phase3.c Normal file
View file

@ -0,0 +1,105 @@
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#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;
}

BIN
project1/phase4/mistake.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
project1/phase4/output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

124
project1/phase4/phase4.c Normal file
View file

@ -0,0 +1,124 @@
#include "bits/time.h"
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#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;
}

177
project1/report/report.tex Normal file
View file

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

1
project1/test.nix Normal file
View file

@ -0,0 +1 @@
{}