initial commit

This commit is contained in:
PowerUser64 2024-11-19 05:54:18 -08:00
commit 77861709f8
7 changed files with 296 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
# build artifacts
*.out
# lsp stuff
compile_commands.json
.cache

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2024 PowerUser64 <blake@blakenorth.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

34
README.md Normal file
View file

@ -0,0 +1,34 @@
# mutex idea
This is the implementation of an idea I had to make a mutex for thread
synchronization without using CPU features (which is apparently what most mutex
implementations use, or are supposed to use). As such, this sketch might have
hidden limitations that I'm not aware of due to my little experience with
parallel programming. If I find any, I'll note them somewhere in this document
or fix them.
## Does it work?
Yes! For this test case, at least. I don't know what use cases would break it.
I verified that it works like it should by running it 10,000+ times on my
machine.
## How?
- A thread manager sets up, spawns, and manages some threads
- Threads ask for the mutex by setting a bool in the thread manager
- The thread manager constantly checks for threads that want the mutex
- It gives it to them when no threads are using it
- Threads tell the thread manager when they are done with the mutex
- Before asking for it again, they wait for the thread manager to tell them
they've transferred it
## TODO's
- [ ] make into a c++ class
- [ ] make an interface for the thread so you can use it to parallelize things
without having to write a thread task function
- [ ] benchmark against a "normal" mutex
- [ ] execution time
- [ ] cpu usage
- [ ] memory usage

2
compile-debug.sh Executable file
View file

@ -0,0 +1,2 @@
#!/bin/bash
g++ main.cpp -lpthread -g -O0 -o mutex.out

2
compile-release.sh Executable file
View file

@ -0,0 +1,2 @@
#!/bin/bash
g++ main.cpp -lpthread -O3 -o mutex.out

205
main.cpp Normal file
View file

@ -0,0 +1,205 @@
#include <cstddef> // size_t
#include <cstring> // memset
#include <iostream> // cout
#include <vector> // vector
// used for threads
#include <pthread.h> // pthread
#include <unistd.h> // sleep() - testing
#define NUM_THREADS 8
typedef void *(*ThreadTask)(const struct thread_data &);
typedef void *(*PthreadFun)(void *arg);
struct thread_group {
bool wants_mutex[NUM_THREADS] = {0};
bool has_mutex[NUM_THREADS] = {0};
bool manager_took_mutex[NUM_THREADS] = {0};
bool threads_finished[NUM_THREADS] = {0};
ThreadTask task;
size_t total_threads;
void *data;
};
struct thread_data {
size_t id;
bool *wants_mutex;
const bool *has_mutex;
bool *manager_took_mutex;
bool *is_finished;
void *data;
};
void *thread_task_increment(const struct thread_data &thread) {
// exists inside the loop
const size_t max_loops = 10;
size_t loops = 0;
std::cout << "Hello from thread " << thread.id << "! (before run loop)"
<< std::endl;
// thread loop, with an exit condition
while (!(*thread.is_finished)) {
// non-mutex operations
{
std::cout << "Hello from thread " << thread.id
<< "! (inside run loop, before mutex)" << std::endl;
}
// mutex
{
// wait for the thread manager to take the mutex if it hasn't yet
while (!*thread.manager_took_mutex)
;
// tell the thread manager we want the mutex
(*thread.wants_mutex) = true;
// block until we have the mutex
while (!*thread.has_mutex)
;
// enter the mutex
std::cout << "Hello from thread " << thread.id
<< "! (inside run loop, inside mutex)" << std::endl;
*static_cast<int *>(thread.data) += 1;
// tell the thread manager we don't need the mutex
(*thread.wants_mutex) = false;
// wait until the thread manager takes the mutex from us
*thread.manager_took_mutex = false;
// mutex exit
}
loops++;
if (max_loops < loops)
(*thread.is_finished) = true;
}
std::cout << "Hello from thread " << thread.id << "! (done)" << std::endl;
pthread_exit(nullptr);
}
void do_threading(struct thread_group threads) {
std::vector<struct thread_data> thread_data(threads.total_threads);
std::vector<pthread_t> my_pthreads(threads.total_threads);
// initialize (might already be done.) TODO: figure out if this is needed
// threads are responsible for telling us when they're blocked
// start of the mutex
memset(threads.wants_mutex, 0,
threads.total_threads * sizeof(*threads.wants_mutex));
// no threads are using the mutex to start with
memset(threads.has_mutex, 0,
threads.total_threads * sizeof(*threads.has_mutex));
// no threads are finished at the start
memset(threads.threads_finished, 0,
threads.total_threads * sizeof(*threads.threads_finished));
// at the start, we have the mutex and haven't given it
memset(threads.manager_took_mutex, 1,
threads.total_threads * sizeof(*threads.manager_took_mutex));
// create thread data
for (size_t tid = 0; tid < threads.total_threads; ++tid) {
struct thread_data this_thread_data = {
.id = tid,
.wants_mutex = &threads.wants_mutex[tid],
.has_mutex = &threads.has_mutex[tid],
.manager_took_mutex = &threads.manager_took_mutex[tid],
.is_finished = &threads.threads_finished[tid],
.data = threads.data,
};
thread_data[tid] = this_thread_data;
}
// spawn threads (none will enter the mutex yet)
for (size_t tid = 0; tid < threads.total_threads; ++tid) {
pthread_create(&my_pthreads[tid], NULL,
reinterpret_cast<PthreadFun>(thread_task_increment),
&thread_data[tid]);
}
std::cout << "Threads have been spawned." << std::endl;
// loop until all threads are done
for (size_t finished_threads = 0; finished_threads < threads.total_threads;) {
// TODO: make sure we cycle the mutex through threads round-robin style
// hand off the mutex to threads that want it
for (size_t tid_wants = 0; tid_wants < threads.total_threads; ++tid_wants) {
if (threads.wants_mutex[tid_wants]) {
// in case the mutex isn't used at all
bool mutex_was_found = false;
// find which thread has the mutex and hand it off if it's done
for (size_t tid_has = 0; tid_has < threads.total_threads; ++tid_has) {
if (threads.has_mutex[tid_has]) {
// we found who has the mutex!
mutex_was_found = true;
// is the thread still using the mutex?
if (!threads.wants_mutex[tid_has]) {
// take the mutex from the thread that has it
threads.has_mutex[tid_has] = false;
// give the mutex to the thread that wants it
threads.has_mutex[tid_wants] = true;
}
break; // no need to look at the rest if we found who has the mutex
}
}
// give the thread the mutex if it wasn't found to be in use
if (!mutex_was_found) {
threads.has_mutex[tid_wants] = true;
}
}
}
// tell all threads we're done taking the mutex from whoever had it
for (size_t tid = 0; tid < threads.total_threads; ++tid) {
threads.manager_took_mutex[tid] = true;
}
// find how many threads are done with the mutex
finished_threads = 0;
for (size_t tid = 0; tid < threads.total_threads; ++tid) {
if (threads.threads_finished[tid])
finished_threads += 1;
}
}
// join all threads (just to make sure - they should already be done)
for (size_t tid = 0; tid < threads.total_threads; ++tid) {
pthread_join(my_pthreads[tid], nullptr);
}
}
#define DBG_PRINT(v) #v << ": " << v
int main(void) {
struct thread_group mythreads;
// the count
int count = 0;
std::cout << "Pre: " << DBG_PRINT(count) << std::endl;
mythreads.data = &count;
mythreads.total_threads = NUM_THREADS;
mythreads.task = thread_task_increment;
do_threading(mythreads);
std::cout << "Post: " << DBG_PRINT(count) << std::endl;
}

28
test-sourceme.sh Normal file
View file

@ -0,0 +1,28 @@
#!/bin/bash
# DO NOT RUN THIS, SOURCE IT FROM YOUR TERMINAL (idk why it needs this)
for ((i=0; i < 1000; ++i)); do
if timeout -s INT 0.8 ./mutex.out > /dev/null
# if grep -q 'Post: count: 99'; then
# : # good result
# else
# e='NOT 99!'
# fi
then
: # did not timeout
else
e='TIMEOUT!'
fi
if [ "$e" = "" ]; then
if ((i % 100 == 99)); then
echo -n .
fi
else
echo -n "-ERROR on take ${i}: $e-"
e=
fi
done
echo # final newline