1

I am trying to implement a singleton worker thread which has multiple functions which can be requested by the user. I have seen implementations for worker threads using always the same function but I need different calls. Making a worker thread for each function is incovenient because the functions will share some resources. What I did to make it work is add a queue of function calls where I simply push the function calls onto the queue and trigger the condition variable. The question now becomes whether this implementation is fine because I feel like there is a smarter way to do this.

#include <functional>
#include <queue>
#include <condition_variable>
#include <mutex>
#include <thread>

class Worker_Thread
{
    private:
        Worker_Thread() : th_{}, mtx_{}, q_{}
        {
            th_ = std::thread(&Worker_Thread::run, this);
        }

        std::thread th_;
        std::mutex mtx_;
        std::condition_variable cv_;
        std::queue<std::function<void()>> q_;

        void run();

        void func_a(int val) {};
        void func_b(float val) {};

    public:
        Worker_Thread(Worker_Thread const&) = delete;
        void operator=(Worker_Thread const&) = delete;

        static Worker_Thread& getInstance()
        {
            static Worker_Thread instance;
            return instance;
        }

        void do_a(int val);
        void do_b(float val);
};

void Worker_Thread::run()
{
    while (true) {
        std::unique_lock<std::mutex> lock(mtx_);
        cv_.wait(lock);

        std::function<void()> fnc = std::move(q_.front());
        fnc();
        q_.pop();
    }
}

void Worker_Thread::do_a(int val)
{
    {
        // Push a function onto the queue
        std::lock_guard<std::mutex> lock(mtx_);
        std::function<void()> fnc = std::bind(&Worker_Thread::func_a, this, val);
        q_.push(fnc);
    }
    // Notify the run thread to execute the queued function
    cv_.notify_one();
}

void Worker_Thread::do_b(float val)
{
    {
        // Push a function onto the queue
        std::lock_guard<std::mutex> lock(mtx_);
        std::function<void()> fnc = std::bind(&Worker_Thread::func_b, this, val);
        q_.push(fnc);
    }
    // Notify the run thread to execute the queued function
    cv_.notify_one();
}

int main()
{
    Worker_Thread& wth = Worker_Thread::getInstance();

    wth.do_a(1);
    wth.do_b(2.3);

    return 0;
}
SillyGoose
  • 53
  • 6
  • 1
    Is the code you have *working* (a hard requirement)? Then this seems more like a request for a code-review which should be posted on [the code review SE site](https://codereview.stackexchange.com/). – Some programmer dude Jul 26 '23 at 09:00
  • 2
    Singletons are usually not a good idea (unit testability). Why not give your workerthread an interface (abstract) baseclass? Create one instance in your main function and inject that into the components that need it? Because with threading (and possibly dll's) you will run into static initialization problems. – Pepijn Kramer Jul 26 '23 at 09:04
  • @Someprogrammerdude Thanks for the hint. Didn't know about the site yet but will use that in the future. – SillyGoose Jul 26 '23 at 09:19
  • @PepijnKramer I have heard that singletons are not the standard anymore. Could you elaborate on the structure you propose or may send a link of an example? – SillyGoose Jul 26 '23 at 09:20
  • A simplified approach would be to have a class that can queue std::function functions (and will accept lambdas with captured arguments). e.q. `template Worker_thread::queue(fn_t&& fn)`. So you can call worker_tread.queue([]{func_a(10)};). Create an instance of that workerthread class in main and pass references to it on to classes (modules) that need to use the workerthread. Dependency injection is quite straight forward and there is a lot of documentation (and examples) to be found out there. So no need for me to go into a lot of detail here. – Pepijn Kramer Jul 26 '23 at 09:47
  • Regarding the singleton discussion: [What are drawbacks or disadvantages of singleton pattern?](https://stackoverflow.com/questions/137975/what-are-drawbacks-or-disadvantages-of-singleton-pattern). The usual alternative is to just provide every caller with a `shared_ptr` – Homer512 Jul 26 '23 at 13:21

1 Answers1

2

This is more of a code review, which, incidentally, got its own site. However, a few notes:

  1. Your run function doesn't release the lock while running the function. That will prevent other threads from queueing new work. Your use of a condition variable is also wrong. You have to check the associated condition before waiting. It should look something like this:
void Worker_Thread::run()
{
    while (true) {
        std::function<void()> fnc;
        {
            std::unique_lock<std::mutex> lock(mtx_);
            while(q_.empty())
                cv_.wait(lock);
            fnc = std::move(q_.front());
            q_.pop();
        }
        fnc();
    }
}
  1. You have no way of terminating the thread or ensuring that the queue is processed before the main thread exits. But this would be easy to add in your design by queueing a function that terminates the run method. Then you can join the thread. You may also want to make sure to prevent other threads from queuing work after the close command has been given.

  2. Your do_a and do_b methods should construct the function object before locking the mutex. Constructing a function is expensive since it requires dynamic memory allocation. You should always do as little work as possible while holding a mutex. You should also move it into the queue.

void Worker_Thread::do_a(int val)
{
    std::function<void()> fnc = std::bind(&Worker_Thread::func_a, this, val);
    {
        // Push a function onto the queue
        std::lock_guard<std::mutex> lock(mtx_);
        q_.push(std::move(fnc));
    }
    // Notify the run thread to execute the queued function
    cv_.notify_one();
}
  1. Consider switching from function to packaged_task so that callers can wait on the result. This would also allow you to catch exceptions and return them to the original caller rather than terminating the thread and crashing the program, as it would currently do

  2. Lambdas are cheaper, easier to use and read than bind with method pointers. Consider this pattern instead:

void Worker_Thread::do_a(int val)
{
    std::function<void()> fnc {[=]() -> void {
        this->func_a(val);
    }};
    ...
}

That way you also don't need a separate func_a and can just inline the code.

Homer512
  • 9,144
  • 2
  • 8
  • 25
  • First of all thanks for the extensive tips. Really appreciate that. Didn't know of the code review site unfortunately but will use it from now on. 1: The clarification on the run task is really helpful and I will implement that. 2: I have left it out for minimality but I have a terminate function in place using a boolean to prevent other threads from queueing. 3: Very good point. Will do so 4: Didn't know about 'packaged_task' yet but definitely will look into it. Big thanks again for the extensive help – SillyGoose Jul 26 '23 at 09:28
  • @SillyGoose regarding the code review site: To be fair, your question will get more views here and questions such as "is there a bug here?" or "can this be made faster?" are still fine on SO. Just make your question as explicit as possible in this regard to avoid it being closed or moved – Homer512 Jul 26 '23 at 11:10