Threads, Member Function Pointer und Type Erasure



  • Ich moechte Memberfunktionen innerhalb eines Threads ausfuehren. Dafuer gibt es std::thread . Jedoch soll beim Aufruf des Destruktors automatisch eine Funktion stop aufgerufen werden und mittels join auf das Ende des Threads gewartet werden. Dazu habe ich mir folgendes einfaches Template geschrieben:

    template<typename T, void (T::*Run)(), void (T::*Stop)()>
    struct service
    {
        service(T& runnable) : runnable(runnable), th(Run, std::ref(runnable)){}
    
        ~service(){
            (runnable.*Stop)();
            if (th.joinable()) th.join();
        }
    private:
        T& runnable;
        std::thread th;
    };
    

    Es kann wie folgt aufgerufen werden:

    struct A {
        void run() { .. }
        void stop() { .. }
    };
    
    struct B {
        void exec() { .. }
        void halt() { .. }
    };
    
    A a;
    B b;
    
    service<A, &A::run, &A::stop> service_a(a);
    service<B, &B::exec, &B::halt> service_b(b);
    

    Jetzt moechte ich nicht staendig ein neuen Typ mittels service-Template erstellen, sondern aehnlich wie bei std::thread einfach Funktion und Objekt uebergeben. Beispiel:

    service2 service_a(&A::stop, &A::run, a)
    

    Als Loesung bietet sich Type Erasure an. Als Vorlage diente http://www.cplusplus.com/forum/articles/18756/ :

    class service2
    {
        struct service_concept
        {
            virtual ~service_concept() {}
        };
    
        template<typename T, void (T::*Run)(), void (T::*Stop)()>
        struct service_model : service_concept
        {
            service_model(T& runnable) : runnable(runnable), th(Run, std::ref(runnable)){}
    
            virtual ~service_model(){
                (runnable.*Stop)();
                if (th.joinable()) th.join();
            }
    
        private:
            T& runnable;
            std::thread th;
        };
    
        std::shared_ptr<service_concept> obj;
    
    public:
        template<typename T, void (T::*Run)(), void (T::*Stop)() >
        service2(Stop s, Run, T& runnable)
        : obj( new service_model<T, Run, Stop>(runnable) ){}
    };
    

    Leider bekomme ich als Fehler in Zeile 27 error C2061: syntax error : identifier 'Stop' . Frage: Wie biege ich es so hin, dass es uebersetzt wird?

    Leider kann ich gerade nicht nachschauen, wie std::function , std::bind bzw. std::thread das machen. Unter Visual Studio 11 Beta verwirren mich die Makros fuer Varadic Templates. Alternativ ueberlege ich gerade, wie eine Loesung mittels std::function ausschaut. Grossartig darueber nachgedacht habe ich jetzt noch nicht.



  • struct service
    {
    	typedef std::function<void ()> action;
    
    	template <class F>
        service(F &&run, action stop) : stop(std::move(stop)), th(std::forward<F>(run)){}
    
        ~service(){
    		stop();
            if (th.joinable()) th.join();
        }
    private:
    	action stop;
        std::thread th;
    };
    


  • Wenn du die Memberfunktionszeiger als Funktionsargumente (und nicht Templateargumente) übergibst, sind sie im Konstruktor keine konstanten Ausdrücke mehr und können deshalb nicht als Templateargumente von service_model verwendet werden.

    Du kannst entweder durchgehend nur noch T als Templateparameter haben und die Memberfunktionszeiger als Variablen speichern, oder du schreibst dir ein Funktionstemplate, das die Memberfunktionszeiger als Templateargumente nimmt:

    auto my_service = create_service<&A::run, &A::stop>(a);
    

    Mit std::function bist du grundsätzlich viel flexibler, da du nicht auf Memberfunktionszeiger beschränkt bist. Allerdings kann die Laufzeit aufgrund der dynamisch gespeicherten Funktionen minimal schlechter werden, was man aber durch direktes Hardwiren durch Templates beheben kann (d.h. der Typ der Funktionen wäre dann F statt std::function<void()> ). Zudem ist die Aufrufsyntax ein wenig komplizierter, da du wahrscheinlich bind() oder mem_fn() benutzen wirst.

    Ausserdem: Verwende std::unique_ptr statt std::shared_ptr , du hast schliesslich keinen geteilten Besitz.



  • Nexus schrieb:

    Mit std::function bist du grundsätzlich viel flexibler, da du nicht auf Memberfunktionszeiger beschränkt bist. Allerdings kann die Laufzeit aufgrund der dynamisch gespeicherten Funktionen minimal schlechter werden

    Warum holst du die Performance-Keule heraus? Das ist total unnötig und spielt hier überhaupt keine Rolle, weil das Hantieren mit Threads schon um Größenordnungen teurer ist.



  • TyRoXx schrieb:

    Warum holst du die Performance-Keule heraus? Das ist total unnötig

    Warum "Keule"? Ich habe das nur als Randnotiz hinzugefügt, damit knivil, der den std::function -Ansatz nicht kennt, etwas mehr darüber erfährt. Keine Ahnung, was du für ein Problem damit hast, zumal ich extra "minimal" geschrieben habe.

    TyRoXx schrieb:

    und spielt hier überhaupt keine Rolle, weil das Hantieren mit Threads schon um Größenordnungen teurer ist.

    Schon mal dran gedacht, dass man in ähnliche Situationen kommen könnte, wo es eine Rolle spielt? 🙄



  • Wenn du die Memberfunktionszeiger als Funktionsargumente (und nicht Templateargumente) übergibst, sind sie im Konstruktor keine konstanten Ausdrücke mehr und können deshalb nicht als Templateargumente von service_model verwendet werden.

    Ja, dass habe ich auch gemerkt. Ziel ist es, auf Template-Parameter a la create_service<&A::run, &A::stop>(a) zu verzichten. Meine bisheriege Loesung schaut wie folgt aus:

    class service2
    {
        struct service_concept
        {
            virtual ~service_concept() {}
        };
    
        template<typename T>
        struct service_model : service_concept
        {
            service_model(T& runnable, void (T::*stop)(), void (T::*run)() )
            : runnable(runnable), th(run, std::ref(runnable)), stop(stop), run(run)
            {
                std::cout << "constructor called " << typeid(*this).name() << std::endl;
            }
    
            virtual ~service_model(){
                std::cout << "destructor called " << typeid(*this).name() << std::endl;
                (runnable.*stop)();
                if (th.joinable()) th.join();
            }
    
        private:
            T& runnable;
            std::thread th;
            void (T::*stop)();
            void (T::*run)();
        };
    
        std::unique_ptr<service_concept> obj;
    
    public:
        template<typename T>
        service2(void (T::*stop)(),void (T::*run)(), T& runnable)
        : obj( new service_model<T>(std::forward<T>(runnable), stop, run) )
        {
        }
    };
    

    Leider wird unter Visual Studio 11 Beta gleich nach dem Konstruktor der Destruktor aufgerufen. Und ich weiss nicht warum. Vielleicht kann mir jemand helfen. Der Debugger hat mir nicht geholfen. Ein Minimalbeispiel:

    struct type_a {
        type_a() {
            std::cout << "constructor type_A" << '\n';
            halt = false;
        }
    
        void run() {
            while(!halt){
                std::this_thread::sleep_for(std::chrono::milliseconds(500));
                std::cout << "type_A thread" << '\n';
            }
        }
    
        void stop() {
            halt = true;
        }
    
    private:
        std::atomic<bool> halt;
    };
    struct type_b {
        type_b() {
            std::cout << "constructor type_B" << '\n';
            halt = false;
        }
    
        void run() {
            while (!halt) {
                std::this_thread::sleep_for(std::chrono::milliseconds(600));
                std::cout << "type_B thread" << '\n';
            }
        }
    
        void stop() {
            halt = true;
        }
    
    private:
        std::atomic<bool> halt;
    };
    int _tmain(int argc, _TCHAR* argv[])
    {
        type_a a;
        type_b b;
    
        service2(&type_a::stop, &type_a::run, a);
        std::this_thread::sleep_for(std::chrono::seconds(5));
    
        service2(&type_b::stop, &type_b::run, b);
        std::this_thread::sleep_for(std::chrono::seconds(5));
    
        return 0;
    }
    

    Nochmals zum Ziel: service2 soll moeglichst mit service2(stop_fn, run_fun, obj, param, ...) aufgerufen werden, wobei stop_fn und run_fn Memberfunktionen von obj sind. obj und param... sollen an std::thread uebergeben werden, obj soll nicht gemoved werden, da ich mit dem Objekt gern noch von aussen arbeiten moechte. Die Erweiterung auf Variadic Templates mittels param... ist erstmal icht wichtig, da Visual Studio 11 Beta die nicht unterstuetzt.

    PS: Performance ist nicht so wichtig, da stop und start nur jeweils einmal aufgerufen werden sollen.



  • gleich nach dem Konstruktor der Destruktor aufgerufen

    Ich Dummbatzen. Natuerlich muss ich auch ein Objekt benennen, also service2 s(...) , da sonst nur ein temporaeres Objekt erzeugt und gleich wieder vernichtet wird. Mittlerweile funktioniert alles wie gewuenscht, wobei ich den Ansatz mit std::function noch untersuchen werde. Danke!


Anmelden zum Antworten