简体   繁体   中英

C++ design help for templated virtual function

I'm trying to implement a number of classes based on aa common class that abstracts a thread-pool using boost.threadpool. I've got something that works (in Xcode on osx 10.7.2) but I'm really not sure its good design or if its even safe (largely because of what I've read on-line about the use of virtual member functions with templates). I'm looking for some style advice on the best way to implement something like this. I'm learning as I go along here so I know a lot of this will be 'bad form'...

I have a base class called 'workqueue' like this:

template <typename T> 
class Workqueue{

    private:
        pool            *pThreadPool;

    public:
        Workqueue       (int);        
        void Start      (T);        
        void Schedule   (T);
        virtual bool Process(T) {return true;}
};

template <typename T> Workqueue<T>::Workqueue(int thread_count){
    pThreadPool = new pool(thread_count);
}

template <typename T> void Workqueue<T>::Start(T data){
    pThreadPool->schedule(boost::bind(&Workqueue::Process,this, data));  
    pThreadPool->wait();
}

template <typename T> void Workqueue<T>::Schedule(T data){
    pThreadPool->schedule(boost::bind(&Workqueue::Process,this, data));    
}

I then define a new service based on this class like this:

struct Service1Data{
    string item_data;
};

class MyService : public Workqueue<Service1Data> {    
public:
    MyService       (int);
    bool Process    (Service1Data);        
};

MyService::MyService(int workers) : Workqueue<Service1Data>(workers) {}

bool MyService::Process(Service1Data service_data){
    cout << "in process (" << service_data.item_data << ")" << endl;     
    return true;
}

(I've removed as much of the code to keep it simple so as shown would run forever as it continually submits new work). I use the service like this:

    MyService *service1 = new MyService(5);

    Service1Data x;
    x.item_data = "testing";
    service1->Start(x);

    // will wait until no more work.
    delete service1;

so my specific questions:

firstly (and please be gentle...) is this bad form and is there a much better way to do this? (and why?)

secondly - is this even safe given the virtual/template issues? I read somewhere that it should be safe if the class itself is templated and I think I understand the basic vtable issues - but really not sure of the specifics.

thirdly - the base workqueue class needs to have the member definitions in the 'h' file with the class definition for it to link. Not sure why that would be - I imagine it's a linker issue to do with the virtual/template issues and so makes me nervous.

all help gratefully received..

Chris

I think, you shouldn't mix processing of data with processing of queue .

I mean, you shouldn't have Process method in your Workqueue . Data may process itself, or your queue can get processing function as ( template ?) parameter.

Then you get rid of all your problems with virtual function. YourService class then should agregate Workqueue and may provide process function.

Also, I doubt if you really need Workqueue . You can just use pThreadPool in YourService .

If you need a common interface for services, you should specify it explicitly & separately. Your inheritance chain looks unclear. inheritance means is . Why YourService is Workqueue . I do not believe! I think YourService can use any sort of queue. But usage is aggregation.

EDIT: Code will look like this:

template<typename Processor>
class WorkQueue
{
public:
    WorkQueue(int count, Processor& processor):_processor(processor),_pool(count) {}

    template <typename Data>
    void schedule(const Data& data)
    {
        _pool->schedule(std::bind(&Processor::process,_processor, data));
    }

    template <typename Data>
    void run(const Data& data)
    {
        schedule(data);
        _pool->wait();
    }

private:
    Processor& _processor;
    pool _pool;
};

class Service
{
public:
    virtual void run() = 0;
    virtual ~Service() {}
};

struct ServiceParams
{
    int param;
};

class MyService: public Service
{

    friend class WorkQueue<MyService>;
public:
    MyService(const ServiceParams& params): _params(params), _queue(1, *this) {}
    void run() { return _queue.run(_params); }
private:
    ServiceParams _params;
    WorkQueue<MyService> _queue;

    void process(const ServiceParams& params) {std::cout <<"hello, world\n";}
};

EDIT: I originally considered usage as:

ServiceData data;
Service* service = new MyService(data);
service->run();
delete service;

Little things I can obviously point out:

  • overuse of new and delete when you could create automatic objects.

For example, if your work-queue and the pool have the same lifetime then:

template <typename T>  class Workqueue
{      
    private:
      pool            threadPool;  
 // // etc
};

template< typename T >
Workqueue::Workqueue( int numThreads ) : threadPool( numThreads )
{
}

Your base class needs a virtual destructor, and as it stands Start could call Schedule rather than implement the same line of code (with the boost::bind) twice. Ideally the constructor that takes an int member only will be declared explicit.

You probably need logic to wait for threads to complete.

I think a good design should isolate the queuing and work/task separately. In your design, both are tightly coupled. This design is good if you want to create separate pool for every type of work/task.

Another approach is to create a separate Work class containing the process function. Then your MyService will extend Work . And WorkQueue class will accept Work and by that means any derived class too. This approach is more generic in nature. So same worker queue can accept different type of work/task. Below code illustration will clear more.

Just to add this approach can also be used if you want to have different pool for different type of data. It is more flexible in nature.

template <typename T> 
class Work{
    T data; // contains the actual data to work on
    public:
        Work(T data) : data(data) {} // constructor to init data
        virtual bool Process(T) {return false;} // returns false to tell process failed
        T getData() { return data; } // get the data
};

class MyWork : public Work<Service1Data> {    
public:
    MyService (Service1Data data) : 
      Work(data) {}
    bool Process (Service1Data); // Implement your work specific process func
};

bool MyWork::Process(Service1Data service_data){
    cout << "in process (" << service_data.item_data << ")" << endl;     
    return true;
}

class Workqueue{

    private:
        pool            *pThreadPool;

    public:
        Workqueue       (int);        
        void Start      (Work);        
        void Schedule   (Work);
};

Workqueue::Workqueue(int thread_count){
    pThreadPool = new pool(thread_count);
}

void Workqueue::Start(Work workToDo){
    pThreadPool->schedule(boost::bind(&Work::Process,this, workToDo.getData()));  
    pThreadPool->wait();
}

void Workqueue::Schedule(Work data){
    pThreadPool->schedule(boost::bind(&Work::Process,this, workToDo.getData()));    
}

Usage

Service1Data x;
x.item_data = "testing";
MyWork myWork(x);


Workqueue wq = new Workqueue(5);
wq->Start(myWork);

// will wait until no more work.
delete service1;

Now to achieve different pools for different type of work/task, create two Workqueue with different pool size and then give one only one type of work and other another type of work.

NOTE: Above code might contain syntax errors, it just there to convey the design. Treat it as pseudo code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM