Callbacks to virtual functions

Viewed 322

I am doing some work with threading on an embedded platform. This platform provides a Thread class, and it has a start method that takes a function pointer, like this:

void do_in_parallel() {
    // Some stuff to do in a new thread
}

Thread my_thread;
my_thread.start(do_in_parallel);

The problem is there is no way to pass parameters in.1 I want to solve this by creating an abstract class, call it Thread2, that extends Thread (or it could just have a Thread as instance data).

Thread2 would have a pure virtual function void run() and the goal was to pass that to Thread::start(void*()), except I soon learned that member function pointers have a different type and can't be used like this. I could make run() static, but then I still can't have more than one instance, defeating the whole purpose (not to mention you can't have a virtual static function).

Are there any workarounds that wouldn't involve changing the original Thread class (considering it's a library that I'm stuck with as-is)?


1. Global variables are a usable workaround in many cases, except when instantiating more than one thread from the same function pointer. I can't come up with a way to avoid race conditions in that case.

3 Answers

Write a global thread pool.

It maintains a queue of tasks. These tasks can have state.

Whe you add a task to the queue, you can choose to also request it get a thread immediately. Or you can wait for threads in the pool to be finished what they are doing.

The threads in the pool are created by the provided Thread class, and they get their marching instructions from the pool. For the most part, they should pop tasks, do them, then wait on another task being ready.

If waiting isn't permitted, you could still have some global thread manager that stores state for the threads.

The pool/manager returns the equivalent of a future<T> augmented with whatever features you want. Code that provides tasks interacts with the task through that object instead of the embedded Thread type.

A simple wrapper can be written if locking is permitted

void start(Thread& t, void (*fn)(void*), void* p)
{
    static std::mutex mtx;  // or any other mutex
    static void* sp;
    static void (*sfn)(void*);

    mtx.lock();
    sp = p;
    sfn = fn;

    t.start([]{
        auto p = sp;
        auto fn = sfn;
        mtx.unlock();
        fn(p);
    });
}

This is obviously not going to scale well, all thread creations goes through the same lock, but its likely enough.

Note this is exception-unsafe, but I assume that is fine in embedded systems.

With the wrapper in place

template<typename C>
void start(Thread& t, C& c)
{
    start(t, [](void* p){
        (*(C*)p)();
    }, &c);
}

Which allows any callable to be used. This particular implementation places the responsibility of managing the callable's lifetime on the caller.

You can create your own threaded dispatching mechanism (producer-consumer queue) built around the platform specific thread. I assume that you have the equivalent facilities of mutex and conditional variables/signalling mechanism for the target platform.

  • Create a thread safe queue that can accept function objects.
  • The run method creates a thread and waits on the queue.
  • The calling thread can call post()/invoke() method that simply insert a function object to the queue.
  • The function object can have the necessary arguments passed to the caller thread.
Related