So I created a thread server that creates threads and associates them with a handle so that you can keep specific threads for specific tasks e.g. run all graphics rendering on thread 0, run physics stepping on thread 1 and use thread 2 for downloading a file of the internet etc. Personally I've found it useful in a project of mine and I highly prefer this to a typical job system where you don't have any control over what thread the task is executed on.
I'd like to know what room for improvement there is, especially in optimization and whether or not you think the overhead is justified.
Each thread executes tasks in a task queue, so I made a Thread_Safe_Queue which is just a wrapper for std::queue but with a mutex before read/writes:
template<typename T>
struct Thread_Safe_Queue {
Thread_Safe_Queue() = default;
Thread_Safe_Queue(Thread_Safe_Queue<T>&& other) noexcept {
std::lock_guard<std::mutex> lock(mutex);
queue = std::move(other.queue);
}
Thread_Safe_Queue(const Thread_Safe_Queue<T>& other) {
std::lock_guard<std::mutex> lock(mutex);
queue = other.queue;
}
virtual ~Thread_Safe_Queue() { }
size_t size() const {
std::lock_guard<std::mutex> lock(mutex);
return queue.size();
}
std::optional<T> pop() {
std::lock_guard<std::mutex> lock(mutex);
if (queue.empty()) {
return {};
}
T tmp = queue.front();
queue.pop();
return tmp;
}
std::optional<T> front() {
std::lock_guard<std::mutex> lock(mutex);
if (queue.empty()) {
return {};
}
return queue.front();
}
void push(const T &item) {
std::lock_guard<std::mutex> lock(mutex);
queue.push(item);
}
bool empty() const {
std::lock_guard<std::mutex> lock(mutex);
return queue.empty();
}
void clear() {
std::lock_guard<std::mutex> lock(mutex);
queue = std::queue<T>();
}
std::queue<T> queue;
mutable std::mutex mutex;
};
Thread_Server.h:
#include "thread_safe_queue.h"
#include <thread>
#include <functional>
typedef unsigned int thread_id_t;
constexpr thread_id_t NULL_THREAD = (thread_id_t)0 - (thread_id_t)1;
typedef std::function<void()> Thread_Task;
struct Thread_Context {
Thread_Safe_Queue<Thread_Task> task_queue;
bool pause;
bool kill = false;
bool dead = false;
};
struct Thread_Server {
Thread_Server();
~Thread_Server();
thread_id_t make_thread(bool start = true);
void pause_thread(thread_id_t tid);
void start_thread(thread_id_t tid);
void kill_thread(thread_id_t tid);
void queue_task(thread_id_t tid, const Thread_Task& task);
void wait_for_thread(thread_id_t tid);
bool is_thread_busy(thread_id_t tid);
std::vector<Thread_Context> _thread_contexts;
};
thread_server.cpp:
#include "thread_server.h"
void work(thread_id_t tid, std::vector<Thread_Context>* pcontexts) {
auto& contexts = *pcontexts;
while (!contexts[tid].kill) {
while (contexts[tid].pause);
auto cmd = contexts[tid].task_queue.front();
if (cmd.has_value()) {
cmd.value()();
contexts[tid].task_queue.pop();
}
}
contexts[tid].dead = true;
}
Thread_Server::Thread_Server() {
}
Thread_Server::~Thread_Server() {
for (int i = 0; i < _thread_contexts.size(); i++) {
wait_for_thread(i);
_thread_contexts[i].kill = true;
}
}
thread_id_t Thread_Server::make_thread(bool start) {
thread_id_t tid = NULL_THREAD;
for (thread_id_t i = 0; i < _thread_contexts.size(); i++) {
if (_thread_contexts[i].dead) {
_thread_contexts[i].dead = false;
_thread_contexts[i].kill = false;
_thread_contexts[i].pause = !start;
_thread_contexts[i].task_queue.clear();
tid = i;
break;
}
}
if (tid == NULL_THREAD) {
tid = (thread_id_t)_thread_contexts.size();
Thread_Context ctx;
ctx.pause = !start;
_thread_contexts.push_back(ctx);
}
std::thread(work, tid, &_thread_contexts).detach();
return tid;
}
void Thread_Server::pause_thread(thread_id_t tid) {
_thread_contexts[tid].pause = true;
}
void Thread_Server::start_thread(thread_id_t tid) {
_thread_contexts[tid].pause = false;
}
void Thread_Server::kill_thread(thread_id_t tid) {
_thread_contexts[tid].kill = true;
}
void Thread_Server::queue_task(thread_id_t tid, const Thread_Task& task) {
auto& ctx = _thread_contexts[tid];
ctx.task_queue.push(task);
}
void Thread_Server::wait_for_thread(thread_id_t tid) {
auto& ctx = _thread_contexts[tid];
while (ctx.task_queue.size() > 0);
}
bool Thread_Server::is_thread_busy(thread_id_t tid) {
return _thread_contexts[tid].task_queue.size() > 0;
}
As you can see there are also functions to pause/start/kill and wait for a specific thread. This can be used to sync threads and wait for return values etc.
As a use case example, you could have one thread for rendering a loading symbol until another thread is finished e.g. downloading a file from the internet and then using the downloaded file:
void update() {
if (want_to_download_something) {
thread_server.queue_task(download_thread, [url]() {
download_result = download(url);
});
}
if (thread_server.is_thread_busy(download_thread)) {
render_loading_icon("Downloading file");
} else if (download_result) {
do_something(download_result);
}
}
And here's a quick test to make sure it's working correctly:
#include <iostream>
#include "thread_server.h"
int main()
{
Thread_Server thread_server;
auto t1 = thread_server.make_thread();
auto t2 = thread_server.make_thread();
auto t3 = thread_server.make_thread();
for (int i = 0; i < 10; i++) {
thread_server.queue_task(t1, []() {
std::cout << "\nHello from thread 1 (std::this_thread::get_id(): " << std::this_thread::get_id() << " )\n";
});
thread_server.queue_task(t2, []() {
std::cout << "\nHello from thread 2 (std::this_thread::get_id(): " << std::this_thread::get_id() << " )\n";
});
thread_server.queue_task(t3, []() {
std::cout << "\nHello from thread 3 (std::this_thread::get_id(): " << std::this_thread::get_id() << " )\n";
});
}
std::cin.get();
}
Edit: I'm fairly new to multithreading so if you have anything to say about the safety of my code I'll be more than glad to hear it.