commit 77861709f882f6e3ac31ba92f06a802a9eebebba Author: PowerUser64 Date: Tue Nov 19 05:54:18 2024 -0800 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88c02b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# build artifacts +*.out + +# lsp stuff +compile_commands.json +.cache diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d17dbf3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 PowerUser64 + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..49bcaec --- /dev/null +++ b/README.md @@ -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 diff --git a/compile-debug.sh b/compile-debug.sh new file mode 100755 index 0000000..d77ed8e --- /dev/null +++ b/compile-debug.sh @@ -0,0 +1,2 @@ +#!/bin/bash +g++ main.cpp -lpthread -g -O0 -o mutex.out diff --git a/compile-release.sh b/compile-release.sh new file mode 100755 index 0000000..ea6fea0 --- /dev/null +++ b/compile-release.sh @@ -0,0 +1,2 @@ +#!/bin/bash +g++ main.cpp -lpthread -O3 -o mutex.out diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..2391d85 --- /dev/null +++ b/main.cpp @@ -0,0 +1,205 @@ +#include // size_t +#include // memset +#include // cout +#include // vector + +// used for threads +#include // pthread +#include // 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(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 thread_data(threads.total_threads); + std::vector 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(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; +} diff --git a/test-sourceme.sh b/test-sourceme.sh new file mode 100644 index 0000000..4a2da65 --- /dev/null +++ b/test-sourceme.sh @@ -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