feat: implemented simulation multithreading

This commit is contained in:
Sara Gerretsen 2025-09-22 19:18:00 +02:00
parent 34a289d36b
commit 99ec92ba88
3 changed files with 129 additions and 4 deletions

View file

@ -1,10 +1,12 @@
#include "simulation.h" #include "simulation.h"
#include "input.h" #include "input.h"
#include "thread_pool.h"
#include <SDL3/SDL_log.h> #include <SDL3/SDL_log.h>
#include <SDL3/SDL_render.h> #include <SDL3/SDL_render.h>
#include <algorithm> #include <algorithm>
#include <clay/clay.h> #include <clay/clay.h>
#include <cstdint> #include <cstdint>
#include <mutex>
#include <set> #include <set>
#include <vector> #include <vector>
@ -73,21 +75,33 @@ static size_t CountNeighbors(Cell const &cell) {
return count; return count;
} }
static void PopulateChanges() { static std::mutex underpopulatedMutex, overpopulatedMutex, bornMutex;
static void FindOverpopulated() {
overpopulatedMutex.lock();
overpopulated.clear(); overpopulated.clear();
underpopulated.clear();
born.clear();
// TODO: consider multithreading, this is highly parallelisable
std::ranges::copy_if(living, std::back_inserter(overpopulated), std::ranges::copy_if(living, std::back_inserter(overpopulated),
[&](Cell const &c) -> bool { [&](Cell const &c) -> bool {
size_t const neighbors{ CountNeighbors(c) }; size_t const neighbors{ CountNeighbors(c) };
return neighbors > 3; return neighbors > 3;
}); });
overpopulatedMutex.unlock();
}
static void FindUnderpopulated() {
underpopulatedMutex.lock();
underpopulated.clear();
std::ranges::copy_if(living, std::back_inserter(underpopulated), std::ranges::copy_if(living, std::back_inserter(underpopulated),
[&](Cell const &c) -> bool { [&](Cell const &c) -> bool {
size_t const neighbors{ CountNeighbors(c) }; size_t const neighbors{ CountNeighbors(c) };
return neighbors < 2; return neighbors < 2;
}); });
underpopulatedMutex.unlock();
}
static void FindBorn() {
bornMutex.lock();
born.clear();
for (Cell const &cell : living) { for (Cell const &cell : living) {
std::vector<Cell> neighbors{ NeighborSet(cell) }; std::vector<Cell> neighbors{ NeighborSet(cell) };
std::ranges::copy_if(neighbors, std::back_inserter(born), std::ranges::copy_if(neighbors, std::back_inserter(born),
@ -96,6 +110,19 @@ static void PopulateChanges() {
return !living.contains(c) && neighbors == 3; return !living.contains(c) && neighbors == 3;
}); });
} }
bornMutex.unlock();
}
static void PopulateChanges() {
#if 1
threading::tasks.ScheduleTask(&FindOverpopulated);
threading::tasks.ScheduleTask(&FindUnderpopulated);
threading::tasks.ScheduleTask(&FindBorn);
#else
FindOverpopulated();
FindUnderpopulated();
FindBorn();
#endif
} }
void InitializeRandom(size_t livingChance, int64_t fillArea) { void InitializeRandom(size_t livingChance, int64_t fillArea) {
@ -115,15 +142,21 @@ void InitializeRandom(size_t livingChance, int64_t fillArea) {
} }
void Step() { void Step() {
underpopulatedMutex.lock();
for (Cell const &cell : underpopulated) { for (Cell const &cell : underpopulated) {
living.erase(cell); living.erase(cell);
} }
underpopulatedMutex.unlock();
overpopulatedMutex.lock();
for (Cell const &cell : overpopulated) { for (Cell const &cell : overpopulated) {
living.erase(cell); living.erase(cell);
} }
overpopulatedMutex.unlock();
bornMutex.lock();
for (Cell const &cell : born) { for (Cell const &cell : born) {
living.insert(cell); living.insert(cell);
} }
bornMutex.unlock();
PopulateChanges(); PopulateChanges();
} }
@ -150,24 +183,30 @@ void Draw(SDL_Renderer *renderer, double cellSizePercent) {
if (!drawDebugInfo) { if (!drawDebugInfo) {
return; return;
} }
overpopulatedMutex.lock();
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255); SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
for (Cell const &cell : overpopulated) { for (Cell const &cell : overpopulated) {
cellRect.x = (viewOffset.x + cell.x) * cellRect.w; cellRect.x = (viewOffset.x + cell.x) * cellRect.w;
cellRect.y = (viewOffset.y + cell.y) * cellRect.h; cellRect.y = (viewOffset.y + cell.y) * cellRect.h;
SDL_RenderRect(renderer, &cellRect); SDL_RenderRect(renderer, &cellRect);
} }
overpopulatedMutex.unlock();
underpopulatedMutex.lock();
SDL_SetRenderDrawColor(renderer, 255, 0, 255, 255); SDL_SetRenderDrawColor(renderer, 255, 0, 255, 255);
for (Cell const &cell : underpopulated) { for (Cell const &cell : underpopulated) {
cellRect.x = (viewOffset.x + cell.x) * cellRect.w; cellRect.x = (viewOffset.x + cell.x) * cellRect.w;
cellRect.y = (viewOffset.y + cell.y) * cellRect.h; cellRect.y = (viewOffset.y + cell.y) * cellRect.h;
SDL_RenderRect(renderer, &cellRect); SDL_RenderRect(renderer, &cellRect);
} }
underpopulatedMutex.unlock();
bornMutex.lock();
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
for (Cell const &cell : born) { for (Cell const &cell : born) {
cellRect.x = (viewOffset.x + cell.x) * cellRect.w; cellRect.x = (viewOffset.x + cell.x) * cellRect.w;
cellRect.y = (viewOffset.y + cell.y) * cellRect.h; cellRect.y = (viewOffset.y + cell.y) * cellRect.h;
SDL_RenderRect(renderer, &cellRect); SDL_RenderRect(renderer, &cellRect);
} }
bornMutex.unlock();
} }
void SetSimulationHovered(bool value) { void SetSimulationHovered(bool value) {

56
src/thread_pool.cpp Normal file
View file

@ -0,0 +1,56 @@
#include "thread_pool.h"
#include <algorithm>
#include <thread>
namespace threading {
ThreadPool::ThreadPool() {
size_t const thread_count{ std::max(std::thread::hardware_concurrency() / 2, 1u) };
this->threads.reserve(thread_count);
for (size_t i{ 0 }; i < thread_count; ++i) {
this->threads.emplace_back(std::bind(&ThreadPool::ThreadFn, this));
}
}
ThreadPool::~ThreadPool() {
this->lock.lock();
this->shutdown = true;
this->threadNotifier.notify_all();
this->lock.unlock();
for (std::thread &thread : this->threads) {
thread.join();
}
}
size_t ThreadPool::GetThreadCount() {
return this->threads.size();
}
void ThreadPool::ScheduleTask(TaskFunc fn) {
this->lock.lock();
this->taskQueue.emplace(std::move(fn));
this->threadNotifier.notify_one();
this->lock.unlock();
}
void ThreadPool::ThreadFn() {
TaskFunc function;
for(;;) {
{
std::unique_lock<std::mutex> lock{ this->lock };
while (!this->shutdown && this->taskQueue.empty()) {
this->threadNotifier.wait(lock);
}
if (this->taskQueue.empty()) {
return;
}
function = std::move(this->taskQueue.front());
this->taskQueue.pop();
}
function.operator()();
}
}
ThreadPool tasks{};
}

30
src/thread_pool.h Normal file
View file

@ -0,0 +1,30 @@
#ifndef THREAD_POOL_H
#define THREAD_POOL_H
#include <condition_variable>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>
namespace threading {
typedef std::function<void()> TaskFunc;
class ThreadPool {
public: ThreadPool();
~ThreadPool();
void ScheduleTask(TaskFunc jobs);
size_t GetThreadCount();
private:
void ThreadFn();
std::queue<TaskFunc> taskQueue{};
std::vector<std::thread> threads{};
std::mutex lock{};
std::condition_variable threadNotifier{};
bool shutdown{ false };
};
extern ThreadPool tasks;
}
#endif // !THREAD_POOL_H