1

I have been trying to create a thread pool class(for personal experimentation/use/fun). I found a way to accept any function with any arguments/return type by using parameter packs (seen in code below) and binding the functions to std::function. This works without issue. The problem is that I want to attempt to make a member function to retrieve returned values from the jobs being executed. I do not know how to make the code generic while attempting to do this.

So far I have tried making a map, that uses the jobs ID as a key and will have the return value of that job stored in it. My struggle is 1. I cant figure out how to determine the type (I can with "typename std::invoke_result::type" but this breaks down if the type is void) 2. How to make a map that can have the Job ID as a key and any type so that I can put return types there.

class ThreadPool{
public:

    //getInstance to allow the second constructor to be called
    static ThreadPool& getInstance(int numThreads){
        static ThreadPool instance(numThreads);

        return instance;
    }

    //add any arg # function to queue
    template <typename Func, typename... Args >
    inline uint64_t push(Func& f, Args&&... args){
        auto funcToAdd = std::bind(f, args...);



        uint64_t newID = currentID++;
        std::unique_lock<std::mutex> lock(JobMutex);

        JobQueue.push(std::make_pair(funcToAdd, newID));
        thread.notify_one();
        return newID; //return the ID of the job in the queue
    }


    /* utility functions will go here*/
    inline void resize(int newTCount){

        int tmp = MAX_THREADS;
        if(newTCount > tmp || newTCount < 1){
            throw bad_thread_alloc("Cannot allocate " + std::to_string(newTCount) + " threads because it is greater than your systems maximum of " + std::to_string(tmp), __FILE__, __LINE__);
        }

        numThreads = (uint8_t)newTCount;
        Pool.resize(newTCount);
        DEBUG("New size is: " + std::to_string(Pool.size()));
    }

    inline uint8_t getThreadCount(){
        return numThreads;
    }

        //how i want the user to interact with this class is
        // int id = push(func, args);
        // auto value = getReturnValue(id); //blocks until return value is returned 
    auto getReturnValue(uint64_t jobID) {
        //Not sure how to handle this
    }

private:

    uint64_t currentID;
    uint8_t numThreads;
    std::vector<std::thread> Pool; //the actual thread pool
    std::queue<std::pair<std::function<void()>, uint64_t>> JobQueue; //the jobs with their assigned ID
    std::condition_variable thread;
    std::mutex JobMutex;

    /* infinite loop function */
    void threadManager();

    /*  Constructors */
    ThreadPool(); //prevent default constructor from being called

    //real constructor that is used
    inline ThreadPool(uint8_t numThreads) : numThreads(numThreads) {
        currentID = 0; //initialize currentID
        int tmp = MAX_THREADS;
        if(numThreads > tmp){
            throw bad_thread_alloc("Cannot allocate " + std::to_string(numThreads) + " threads because it is greater than your systems maximum of " + std::to_string(tmp), __FILE__, __LINE__);
        }
        for(int i = 0; i != numThreads; ++i){
            Pool.push_back(std::thread(&ThreadPool::threadManager, this));
            Pool.back().detach();
            DEBUG("Thread " + std::to_string(i) + " allocated");
        }
        DEBUG("Number of threads being allocated " + std::to_string(numThreads));
    }
    /* end constructors */


NULL_COPY_AND_ASSIGN(ThreadPool);
}; /* end ThreadPool Class */


void ThreadPool::threadManager(){
    while (true) {

        std::unique_lock<std::mutex> lock(JobMutex);
        thread.wait(lock, [this] {return !JobQueue.empty(); });

        //strange bug where it will continue even if the job queue is empty
        if (JobQueue.size() < 1)
            continue;

        auto job = JobQueue.front().first;
        JobQueue.pop();
        job();
    }
}

Am I going about this all wrong? I didnt know any other way to generically store any type of function while also being able to get return types out of them.

Paul
  • 11
  • 1
  • 3
    Something like [`std::future`](https://en.cppreference.com/w/cpp/thread/future) and [`std::promise`](https://en.cppreference.com/w/cpp/thread/promise)? – πάντα ῥεῖ Jun 01 '19 at 06:58
  • It will be impossible to return different values from a member function like `getReturnValue`. Better way to deal with this is return a future/promise from `push`. There you can deduce return-type based on the function passed in. – super Jun 01 '19 at 07:10
  • So I could return a future/promise from push, and set the future/promise in the infinite loop function with the value returned when the job is ran? That is an interesting idea. How would this work with a void return type though? I have trouble wrapping my head around the idea of futures/promises so I am not the best with them! The more explanation the better. – Paul Jun 01 '19 at 07:16

1 Answers1

0

Provide a location to store the return value to the function that creates the job. If we provide the return location together with the other elements, then there's no uncertainty about where the job gets stored.

We can design this part of the interface relatively simply. If we package the arguments together with the function, we can store all queued jobs as std::function<void()>. This simplifies the implementation of the thread queue, since the thread queue only has to worry about one type of std::function, and doesn't have to care at all about the return value.

using job_t = std::function<void()>; 

template<class Return, class Func, class... Args>
job_t createJob(Return& dest, Func&& func, Args&&... args) {
    return job_t([=]() {
        dest = func(std::forward<Args>(args)...); 
    });
};

This implementation makes it easy to start jobs asynchronously at will, without the thread pool worrying about where the return value gets stored. As an example, I wrote a function to run a job. The function starts the job on a new thread, and it marks an atomic_bool as true when the function completes. Because we don't have to worry about the return value, the function only has to return a shared_ptr to the atomic_bool we use to check if the function's finished.

using std::shared_ptr; 
using std::atomic_bool; 

shared_ptr<atomic_bool> run_job_async(std::function<void()> func) {
    shared_ptr<atomic_bool> is_complete = std::make_shared<atomic_bool>(false); 
    std::thread t([f = std::move(func), =]() {
        f();
        *is_complete = true;
    }); 
    t.detach(); 
    return is_complete; 
}
Alecto Irene Perez
  • 10,321
  • 23
  • 46
  • I think this is a good solution, but how would this work with a function that returns void? Seems like it would break. Also, I am trying to prevent a thread from being started every time a job comes up. Constantly setting up and tearing down threads would make the threadpool slow down execution times. – Paul Jun 01 '19 at 16:13