blt-gp/include/blt/gp/threading.h

242 lines
7.7 KiB
C++

#pragma once
/*
* Copyright (C) 2024 Brett Terpstra
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef BLT_GP_THREADING_H
#define BLT_GP_THREADING_H
#include <blt/std/types.h>
#include <blt/std/thread.h>
#include <thread>
#include <functional>
#include <atomic>
#include <type_traits>
namespace blt::gp
{
namespace detail
{
struct empty_callable
{
void operator()() const
{
}
};
}
template <typename EnumId>
class task_builder_t;
template <typename EnumId, typename Parallel, typename Single = detail::empty_callable>
class task_t
{
static_assert(std::is_enum_v<EnumId>, "Enum ID must be of enum type!");
static_assert(std::is_invocable_v<Parallel, int>, "Parallel must be invocable with exactly one argument (thread index)");
static_assert(std::is_invocable_v<Single>, "Single must be invocable with no arguments");
friend task_builder_t<EnumId>;
public:
task_t(const EnumId task_id, const Parallel& parallel, const Single& single): parallel(std::forward<Parallel>(parallel)),
single(std::forward<Single>(single)),
requires_single_sync(true), task_id(task_id)
{
}
explicit task_t(const EnumId task_id, const Parallel& parallel): parallel(std::forward<Parallel>(parallel)), single(detail::empty_callable{}),
task_id(task_id)
{
}
void call_parallel(size_t thread_index) const
{
parallel(thread_index);
}
void call_single() const
{
single();
}
[[nodiscard]] EnumId get_task_id() const
{
return task_id;
}
private:
const Parallel& parallel;
const Single& single;
bool requires_single_sync = false;
EnumId task_id;
};
template <typename EnumId, typename Parallel, typename Single = detail::empty_callable>
task_t(EnumId, Parallel, Single) -> task_t<EnumId, Parallel, Single>;
template <typename EnumId>
class task_builder_t
{
static_assert(std::is_enum_v<EnumId>, "Enum ID must be of enum type!");
using EnumInt = std::underlying_type_t<EnumId>;
public:
task_builder_t() = default;
template <typename... Tasks>
static std::function<void(barrier&, EnumId, size_t)> make_callable(Tasks&&... tasks)
{
return [&tasks...](barrier& sync_barrier, EnumId task, size_t thread_index)
{
call_jmp_table(sync_barrier, task, thread_index, tasks...);
};
}
private:
template <typename Task>
static void execute(barrier& sync_barrier, const size_t thread_index, Task&& task)
{
// sync_barrier.wait();
if (task.requires_single_sync)
{
if (thread_index == 0)
task.call_single();
sync_barrier.wait();
}
task.call_parallel(thread_index);
// sync_barrier.wait();
}
template <typename Task>
static bool call(barrier& sync_barrier, const EnumId current_task, const size_t thread_index, Task&& task)
{
if (static_cast<EnumInt>(current_task) == static_cast<EnumInt>(task.get_task_id()))
{
execute(sync_barrier, thread_index, std::forward<Task>(task));
return false;
}
return true;
}
template <typename... Tasks>
static void call_jmp_table(barrier& sync_barrier, const EnumId current_task, const size_t thread_index, Tasks&&... tasks)
{
if (static_cast<EnumInt>(current_task) >= sizeof...(tasks))
BLT_UNREACHABLE;
(call(sync_barrier, current_task, thread_index, std::forward<Tasks>(tasks)) && ...);
}
};
template <typename EnumId>
class thread_manager_t
{
static_assert(std::is_enum_v<EnumId>, "Enum ID must be of enum type!");
public:
explicit thread_manager_t(const size_t thread_count, std::function<void(barrier&, EnumId, size_t)> task_func,
const bool will_main_block = true): barrier(thread_count), will_main_block(will_main_block)
{
thread_callable = [this, task_func = std::move(task_func)](const size_t thread_index)
{
while (should_run)
{
barrier.wait();
if (tasks_remaining > 0)
task_func(barrier, tasks.back(), thread_index);
barrier.wait();
if (thread_index == 0)
{
if (this->will_main_block)
{
tasks.pop_back();
--tasks_remaining;
}
else
{
std::scoped_lock lock{task_lock};
tasks.pop_back();
--tasks_remaining;
}
}
}
};
for (size_t i = 0; i < will_main_block ? thread_count - 1 : thread_count; ++i)
threads.emplace_back(thread_callable, will_main_block ? i + 1 : i);
}
void execute() const
{
BLT_ASSERT(will_main_block &&
"You attempted to call this function without specifying that "
"you want an external blocking thread (try passing will_main_block = true)");
thread_callable(0);
}
void add_task(EnumId task)
{
if (will_main_block)
{
tasks.push_back(task);
++tasks_remaining;
}
else
{
std::scoped_lock lock(task_lock);
tasks.push_back(task);
++tasks_remaining;
}
}
bool has_tasks_left()
{
if (will_main_block)
{
return !tasks.empty();
}
std::scoped_lock lock{task_lock};
return tasks.empty();
}
~thread_manager_t()
{
should_run = false;
for (auto& thread : threads)
{
if (thread.joinable())
thread.join();
}
}
private:
[[nodiscard]] size_t thread_count() const
{
return will_main_block ? threads.size() + 1 : threads.size();
}
blt::barrier barrier;
std::atomic_bool should_run = true;
bool will_main_block;
std::vector<EnumId> tasks;
std::atomic_uint64_t tasks_remaining = 0;
std::vector<std::thread> threads;
std::mutex task_lock;
std::function<void(size_t)> thread_callable;
};
}
#endif //BLT_GP_THREADING_H