Import roar11's thread pool
See https://roar11.com/2016/01/a-platform-independent-thread-pool-using-c14/
This commit is contained in:
parent
fb69fd2289
commit
f6cc2ff951
2 changed files with 373 additions and 0 deletions
239
src/roar11/ThreadPool.hpp
Normal file
239
src/roar11/ThreadPool.hpp
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
/**
|
||||||
|
* The ThreadPool class.
|
||||||
|
* Keeps a set of threads constantly waiting to execute incoming jobs.
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifndef THREADPOOL_HPP
|
||||||
|
#define THREADPOOL_HPP
|
||||||
|
|
||||||
|
#include "ThreadSafeQueue.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <future>
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace roar11
|
||||||
|
{
|
||||||
|
class ThreadPool
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
class IThreadTask
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
IThreadTask(void) = default;
|
||||||
|
virtual ~IThreadTask(void) = default;
|
||||||
|
IThreadTask(const IThreadTask& rhs) = delete;
|
||||||
|
IThreadTask& operator=(const IThreadTask& rhs) = delete;
|
||||||
|
IThreadTask(IThreadTask&& other) = default;
|
||||||
|
IThreadTask& operator=(IThreadTask&& other) = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the task.
|
||||||
|
*/
|
||||||
|
virtual void execute() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Func>
|
||||||
|
class ThreadTask: public IThreadTask
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ThreadTask(Func&& func)
|
||||||
|
:m_func{std::move(func)}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
~ThreadTask(void) override = default;
|
||||||
|
ThreadTask(const ThreadTask& rhs) = delete;
|
||||||
|
ThreadTask& operator=(const ThreadTask& rhs) = delete;
|
||||||
|
ThreadTask(ThreadTask&& other) = default;
|
||||||
|
ThreadTask& operator=(ThreadTask&& other) = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the task.
|
||||||
|
*/
|
||||||
|
void execute() override
|
||||||
|
{
|
||||||
|
m_func();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Func m_func;
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* A wrapper around a std::future that adds the behavior of futures returned from std::async.
|
||||||
|
* Specifically, this object will block and wait for execution to finish before going out of scope.
|
||||||
|
*/
|
||||||
|
template <typename T>
|
||||||
|
class TaskFuture
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TaskFuture(std::future<T>&& future)
|
||||||
|
:m_future{std::move(future)}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskFuture(const TaskFuture& rhs) = delete;
|
||||||
|
TaskFuture& operator=(const TaskFuture& rhs) = delete;
|
||||||
|
TaskFuture(TaskFuture&& other) = default;
|
||||||
|
TaskFuture& operator=(TaskFuture&& other) = default;
|
||||||
|
~TaskFuture(void)
|
||||||
|
{
|
||||||
|
if(m_future.valid())
|
||||||
|
{
|
||||||
|
m_future.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto get(void)
|
||||||
|
{
|
||||||
|
return m_future.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::future<T> m_future;
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
ThreadPool(void)
|
||||||
|
:ThreadPool{std::max(std::thread::hardware_concurrency(), 2u) - 1u}
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Always create at least one thread. If hardware_concurrency() returns 0,
|
||||||
|
* subtracting one would turn it to UINT_MAX, so get the maximum of
|
||||||
|
* hardware_concurrency() and 2 before subtracting 1.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
explicit ThreadPool(const std::uint32_t numThreads)
|
||||||
|
:m_done{false},
|
||||||
|
m_workQueue{},
|
||||||
|
m_threads{}
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for(std::uint32_t i = 0u; i < numThreads; ++i)
|
||||||
|
{
|
||||||
|
m_threads.emplace_back(&ThreadPool::worker, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(...)
|
||||||
|
{
|
||||||
|
destroy();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-copyable.
|
||||||
|
*/
|
||||||
|
ThreadPool(const ThreadPool& rhs) = delete;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-assignable.
|
||||||
|
*/
|
||||||
|
ThreadPool& operator=(const ThreadPool& rhs) = delete;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destructor.
|
||||||
|
*/
|
||||||
|
~ThreadPool(void)
|
||||||
|
{
|
||||||
|
destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit a job to be run by the thread pool.
|
||||||
|
*/
|
||||||
|
template <typename Func, typename... Args>
|
||||||
|
auto submit(Func&& func, Args&&... args)
|
||||||
|
{
|
||||||
|
auto boundTask = std::bind(std::forward<Func>(func), std::forward<Args>(args)...);
|
||||||
|
using ResultType = std::result_of_t<decltype(boundTask)()>;
|
||||||
|
using PackagedTask = std::packaged_task<ResultType()>;
|
||||||
|
using TaskType = ThreadTask<PackagedTask>;
|
||||||
|
|
||||||
|
PackagedTask task{std::move(boundTask)};
|
||||||
|
TaskFuture<ResultType> result{task.get_future()};
|
||||||
|
m_workQueue.push(std::make_unique<TaskType>(std::move(task)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Constantly running function each thread uses to acquire work items from the queue.
|
||||||
|
*/
|
||||||
|
void worker(void)
|
||||||
|
{
|
||||||
|
while(!m_done)
|
||||||
|
{
|
||||||
|
std::unique_ptr<IThreadTask> pTask{nullptr};
|
||||||
|
if(m_workQueue.waitPop(pTask))
|
||||||
|
{
|
||||||
|
pTask->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates the queue and joins all running threads.
|
||||||
|
*/
|
||||||
|
void destroy(void)
|
||||||
|
{
|
||||||
|
m_done = true;
|
||||||
|
m_workQueue.invalidate();
|
||||||
|
for(auto& thread : m_threads)
|
||||||
|
{
|
||||||
|
if(thread.joinable())
|
||||||
|
{
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::atomic_bool m_done;
|
||||||
|
ThreadSafeQueue<std::unique_ptr<IThreadTask>> m_workQueue;
|
||||||
|
std::vector<std::thread> m_threads;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace DefaultThreadPool
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the default thread pool for the application.
|
||||||
|
* This pool is created with std::thread::hardware_concurrency() - 1 threads.
|
||||||
|
*/
|
||||||
|
inline ThreadPool& getThreadPool(void)
|
||||||
|
{
|
||||||
|
static ThreadPool defaultPool;
|
||||||
|
return defaultPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit a job to the default thread pool.
|
||||||
|
*/
|
||||||
|
template <typename Func, typename... Args>
|
||||||
|
inline auto submitJob(Func&& func, Args&&... args)
|
||||||
|
{
|
||||||
|
return getThreadPool().submit(std::forward<Func>(func), std::forward<Args>(args)...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
134
src/roar11/ThreadSafeQueue.hpp
Normal file
134
src/roar11/ThreadSafeQueue.hpp
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
/**
|
||||||
|
* The ThreadSafeQueue class.
|
||||||
|
* Provides a wrapper around a basic queue to provide thread safety.
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifndef THREADSAFEQUEUE_HPP
|
||||||
|
#define THREADSAFEQUEUE_HPP
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <mutex>
|
||||||
|
#include <queue>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace roar11
|
||||||
|
{
|
||||||
|
template <typename T>
|
||||||
|
class ThreadSafeQueue
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Destructor.
|
||||||
|
*/
|
||||||
|
~ThreadSafeQueue(void)
|
||||||
|
{
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to get the first value in the queue.
|
||||||
|
* Returns true if a value was successfully written to the out parameter, false otherwise.
|
||||||
|
*/
|
||||||
|
bool tryPop(T& out)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock{m_mutex};
|
||||||
|
if(m_queue.empty() || !m_valid)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out = std::move(m_queue.front());
|
||||||
|
m_queue.pop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first value in the queue.
|
||||||
|
* Will block until a value is available unless clear is called or the instance is destructed.
|
||||||
|
* Returns true if a value was successfully written to the out parameter, false otherwise.
|
||||||
|
*/
|
||||||
|
bool waitPop(T& out)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock{m_mutex};
|
||||||
|
m_condition.wait(lock, [this]()
|
||||||
|
{
|
||||||
|
return !m_queue.empty() || !m_valid;
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
* Using the condition in the predicate ensures that spurious wakeups with a valid
|
||||||
|
* but empty queue will not proceed, so only need to check for validity before proceeding.
|
||||||
|
*/
|
||||||
|
if(!m_valid)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out = std::move(m_queue.front());
|
||||||
|
m_queue.pop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push a new value onto the queue.
|
||||||
|
*/
|
||||||
|
void push(T value)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock{m_mutex};
|
||||||
|
m_queue.push(std::move(value));
|
||||||
|
m_condition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether or not the queue is empty.
|
||||||
|
*/
|
||||||
|
bool empty(void) const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock{m_mutex};
|
||||||
|
return m_queue.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all items from the queue.
|
||||||
|
*/
|
||||||
|
void clear(void)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock{m_mutex};
|
||||||
|
while(!m_queue.empty())
|
||||||
|
{
|
||||||
|
m_queue.pop();
|
||||||
|
}
|
||||||
|
m_condition.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the queue.
|
||||||
|
* Used to ensure no conditions are being waited on in waitPop when
|
||||||
|
* a thread or the application is trying to exit.
|
||||||
|
* The queue is invalid after calling this method and it is an error
|
||||||
|
* to continue using a queue after this method has been called.
|
||||||
|
*/
|
||||||
|
void invalidate(void)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock{m_mutex};
|
||||||
|
m_valid = false;
|
||||||
|
m_condition.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not this queue is valid.
|
||||||
|
*/
|
||||||
|
bool isValid(void) const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock{m_mutex};
|
||||||
|
return m_valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::atomic_bool m_valid{true};
|
||||||
|
mutable std::mutex m_mutex;
|
||||||
|
std::queue<T> m_queue;
|
||||||
|
std::condition_variable m_condition;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
Loading…
Reference in a new issue