initial commit
This commit is contained in:
commit
2c0dc2aa70
1
.clang-format
Normal file
1
.clang-format
Normal file
@ -0,0 +1 @@
|
||||
BasedOnStyle: WebKit
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
cmake-build-*
|
||||
.idea
|
9
CMakeLists.txt
Normal file
9
CMakeLists.txt
Normal file
@ -0,0 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.19)
|
||||
project(tp_impl)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
add_subdirectory(libs/MyThreadPool)
|
||||
|
||||
add_executable(tp_impl main.cpp)
|
||||
target_link_libraries(tp_impl PRIVATE MyThreadPool)
|
10
libs/MyThreadPool/CMakeLists.txt
Normal file
10
libs/MyThreadPool/CMakeLists.txt
Normal file
@ -0,0 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.19)
|
||||
project(MyThreadPool)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
add_library(MyThreadPool INTERFACE include/SafePriorityQueue.h)
|
||||
|
||||
target_include_directories(MyThreadPool
|
||||
INTERFACE include/
|
||||
)
|
173
libs/MyThreadPool/include/MyThreadPool.h
Normal file
173
libs/MyThreadPool/include/MyThreadPool.h
Normal file
@ -0,0 +1,173 @@
|
||||
#pragma once
|
||||
|
||||
#include "SafePriorityQueue.h"
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace MyThreadPool {
|
||||
|
||||
class ThreadPool {
|
||||
|
||||
public:
|
||||
ThreadPool()
|
||||
: ThreadPool(0)
|
||||
{
|
||||
}
|
||||
|
||||
explicit ThreadPool(size_t workers)
|
||||
: threadCount(workers)
|
||||
{
|
||||
createWorkers();
|
||||
}
|
||||
|
||||
template <typename F, typename... Args, typename R = std::invoke_result_t<std::decay_t<F>, std::decay_t<Args>...>>
|
||||
std::future<R> enqueue(int priority, F&& f, Args&&... args)
|
||||
{
|
||||
using ReturnType = decltype(f(std::forward<Args>(args)...));
|
||||
|
||||
// encapsulate it for copying into the queue
|
||||
auto funcShared = std::make_shared<std::packaged_task<ReturnType()>>(
|
||||
std::bind(std::forward<F>(f), std::forward<Args>(args)...));
|
||||
|
||||
auto fut = funcShared->get_future();
|
||||
|
||||
// wrap the packaged task into a lambda
|
||||
auto wrapperFunc = [task = std::move(funcShared)]() {
|
||||
(*task)();
|
||||
};
|
||||
|
||||
enqueueAndNotify(priority, std::move(wrapperFunc));
|
||||
|
||||
return fut;
|
||||
}
|
||||
|
||||
size_t getTasksLeft() const
|
||||
{
|
||||
return queue.length();
|
||||
}
|
||||
|
||||
size_t getTasksRunning() const
|
||||
{
|
||||
return threadsWorking;
|
||||
}
|
||||
|
||||
size_t getTotalWorkers() const
|
||||
{
|
||||
return threadCount;
|
||||
}
|
||||
|
||||
bool isIdle() const
|
||||
{
|
||||
return queue.empty() && threadsWorking == 0;
|
||||
}
|
||||
|
||||
bool isClosed() const
|
||||
{
|
||||
return done;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for all tasks to finish. Other threads can still new insert tasks into the pool.
|
||||
* Will return upon emptying the queue.
|
||||
*/
|
||||
void waitForTasks(bool untilDone = true)
|
||||
{
|
||||
// Queue will empty when the last task is being processed, not when all tasks finish.
|
||||
while (!queue.empty() || (untilDone && threadsWorking > 0)) {
|
||||
std::unique_lock<std::mutex> poolLock(poolMtx);
|
||||
poolCond.wait(poolLock);
|
||||
}
|
||||
}
|
||||
|
||||
~ThreadPool()
|
||||
{
|
||||
/*
|
||||
* No need to wait until all tasks are truly finished. If, for whatever reason, one of the thread crashes or
|
||||
* doesn't finish execution of the task, we can still join (wait for all threads to finish their tasks) and
|
||||
* gracefully (though with possible corruption?) destroy the pool.
|
||||
*/
|
||||
waitForTasks(false);
|
||||
destroyThreads();
|
||||
}
|
||||
|
||||
private:
|
||||
void enqueueAndNotify(int priority, std::function<void()>&& func)
|
||||
{
|
||||
queue.push(priority, std::move(func));
|
||||
taskCond.notify_one();
|
||||
}
|
||||
|
||||
/**
|
||||
* Worker function for each thread in the pool.
|
||||
*/
|
||||
void worker()
|
||||
{
|
||||
while (!done) {
|
||||
{
|
||||
std::unique_lock<std::mutex> taskLock(taskMtx);
|
||||
taskCond.wait(taskLock, [&]() { return done || !queue.empty(); });
|
||||
}
|
||||
|
||||
// Don't even attempt to do another task if we're destroying the threads. Just bail.
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto callable = std::move(queue.pop());
|
||||
if (callable.has_value()) {
|
||||
threadsWorking++;
|
||||
callable.value()();
|
||||
threadsWorking--;
|
||||
poolCond.notify_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void createWorkers()
|
||||
{
|
||||
assert(threads.empty());
|
||||
|
||||
if (threadCount == 0) {
|
||||
threadCount = std::thread::hardware_concurrency();
|
||||
}
|
||||
|
||||
assert(threadCount > 0);
|
||||
|
||||
threads.reserve(threadCount);
|
||||
for (size_t i = 0; i < threadCount; i++) {
|
||||
threads.emplace_back(std::thread(&ThreadPool::worker, this));
|
||||
}
|
||||
}
|
||||
|
||||
void destroyThreads()
|
||||
{
|
||||
done = true;
|
||||
taskCond.notify_all();
|
||||
for (auto& thread : threads) {
|
||||
if (thread.joinable()) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SafePriorityQueue<std::function<void()>> queue;
|
||||
std::vector<std::thread> threads;
|
||||
size_t threadCount;
|
||||
std::atomic<size_t> threadsWorking { 0 };
|
||||
std::atomic<bool> done { false };
|
||||
|
||||
std::condition_variable taskCond;
|
||||
mutable std::mutex taskMtx;
|
||||
|
||||
std::condition_variable poolCond;
|
||||
mutable std::mutex poolMtx;
|
||||
};
|
||||
|
||||
}
|
90
libs/MyThreadPool/include/SafePriorityQueue.h
Normal file
90
libs/MyThreadPool/include/SafePriorityQueue.h
Normal file
@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
|
||||
#include <forward_list>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
|
||||
namespace MyThreadPool {
|
||||
|
||||
/**
|
||||
* Thread-safe FIFO buffer with insertion based on priority.
|
||||
* Elements with higher priority value are popped first.
|
||||
* Pop is O(1), push is O(n), worst case.
|
||||
*
|
||||
*/
|
||||
template <typename T>
|
||||
class SafePriorityQueue {
|
||||
|
||||
public:
|
||||
void push(int priority, T& obj)
|
||||
{
|
||||
push(priority, std::forward<T>(obj));
|
||||
}
|
||||
|
||||
void push(int priority, T&& obj)
|
||||
{
|
||||
std::unique_lock<std::shared_mutex> lock(mtx);
|
||||
|
||||
auto it = queue.cbegin();
|
||||
auto prevIt = queue.cbefore_begin();
|
||||
// move the iterator until a task with lower priority is found
|
||||
for (; it != queue.cend(); prevIt = it++) {
|
||||
if (it->first < priority) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// even nothing is found, it's okay, we'll append to the list
|
||||
queue.emplace_after(prevIt, std::make_pair(priority, std::forward<T>(obj)));
|
||||
|
||||
queueLength++;
|
||||
}
|
||||
|
||||
std::optional<T> pop()
|
||||
{
|
||||
std::unique_lock<std::shared_mutex> lock(mtx);
|
||||
|
||||
if (queue.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
queueLength--;
|
||||
|
||||
auto ret = std::move(queue.front());
|
||||
queue.pop_front();
|
||||
|
||||
return ret.second;
|
||||
}
|
||||
|
||||
std::optional<T> peek() const
|
||||
{
|
||||
std::shared_lock<std::shared_mutex> lock(mtx);
|
||||
|
||||
if (queue.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return queue.front().second;
|
||||
}
|
||||
|
||||
bool empty() const
|
||||
{
|
||||
std::shared_lock<std::shared_mutex> lock(mtx);
|
||||
return queue.empty();
|
||||
}
|
||||
|
||||
size_t length() const
|
||||
{
|
||||
std::shared_lock<std::shared_mutex> lock(mtx);
|
||||
return queueLength;
|
||||
}
|
||||
|
||||
private:
|
||||
std::forward_list<std::pair<int, T>> queue;
|
||||
mutable std::shared_mutex mtx;
|
||||
size_t queueLength { 0 };
|
||||
};
|
||||
|
||||
} // namespace MyThreadPool
|
37
main.cpp
Normal file
37
main.cpp
Normal file
@ -0,0 +1,37 @@
|
||||
#include <MyThreadPool.h>
|
||||
#include <iostream>
|
||||
|
||||
int main()
|
||||
{
|
||||
MyThreadPool::ThreadPool tp(4);
|
||||
|
||||
auto task = [](int i) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
std::cout << "Testing future " << i << std::endl;
|
||||
return i / 2;
|
||||
};
|
||||
|
||||
tp.enqueue(
|
||||
0, [](int i) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
std::cout << "Test " << i << std::endl;
|
||||
return i / 2;
|
||||
},
|
||||
45);
|
||||
|
||||
auto fut = tp.enqueue(0, task, 10);
|
||||
|
||||
std::cout << "Returned: " << fut.get() << std::endl;
|
||||
|
||||
tp.waitForTasks();
|
||||
|
||||
std::cout << "Done." << std::endl;
|
||||
|
||||
tp.enqueue(0, []() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
std::cout << "Finished with destructor." << std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
Loading…
Reference in New Issue
Block a user