Mehrere std::mutex auf einmal so Ok?



  • Hallo,

    wenn ich eine Klasse (sagen wir hier mal Test ) habe, die von mehreren Threads benutzt werden soll. Dann kann ich ja deren Memberfunktionen mit Hilfe eines lock_guard entsprechen absichern und dadurch thread-safe machen:

    #include <mutex>
    
    class Test
    {
        mutable std::mutex mut, user_mut;
    public:
        void foo()
        {
            std::lock_guard<std::mutex> lock(mut);
            // Do something thread safe
        }
        void bar()
        {
            std::lock_guard<std::mutex> lock(mut);
            // Do something else thread safe
        }
        std::mutex &mutex() const
        {
            return user_mut;
        }
    };
    
    void do_something()
    {
        Test test;
        test.foo();
        test.bar();
    }
    

    Die Funktionen foo und bar sind jetzt damit abgesichert und können von mehreren threads benutzt werden.

    Das Objekt an sich aber nicht. Wenn ich jetzt mehrere Operationen hintereinander an test durchführen will, dann ist zwar jede einzelne für sich sicher, zwischen den Operationen könnten aber andere threads "dazwischenfunken". Teilweise ist es aber (von der Logik her) notwendig, dass diese Funktionen alle seriell ausgeführt werden.

    Deswegen hab ich mir überlegt noch einen zweiten Mutex einzuführen, oben mit user_mut bezeichnet. Dann könnte ein Benutzer folgendes machen um do_something abzusichern:

    void do_something()
    {
        Test test;
        std::lock_guard<std::mutex> lock(test.mutex());
    
        test.foo();
        test.bar();
    }
    

    Jetzt die Frage: ist das so in Ordnung? Weil jetzt werden ja zwei verschiedene Mutex Objekte benutzt und auch gelockt, kann dadurch irgendwas fieses (Deadlock etc) passieren oder passt das so?



  • Deadlock wohl nicht. Aber sicher ist das ja nur, wenn sich alle beteiligten daran halten, immer erst test.mutex zu locken.
    Wo ist dann aber noch der Sinn des internen Mutex?



  • Caligulaminus schrieb:

    Deadlock wohl nicht. Aber sicher ist das ja nur, wenn sich alle beteiligten daran halten, immer erst test.mutex zu locken.

    Ja, das ist ein Problem, allerdings ist mir bis jetzt keine Lösung dafür eingefallen...

    Caligulaminus schrieb:

    Wo ist dann aber noch der Sinn des internen Mutex?

    Der Sinn ist, dass in 95% der Fälle nur eine Funktion an test aufgerufen wird. Dementsprechend nervig wäre es, jedesmal nochmal extra zu locken...



  • Nein, das ist nicht in Ordnung so. Wenn Du nicht konsistent denselben Mutex verwendest, kann es ja wieder zu Datenrennen kommen.

    Du könntest denselben Mutex nutzen, dafür aber einen rekursiven und dann so etwas schreiben:

    class Test
    {
    public:
        typedef std::recursive_mutex mtx_type;
    
        void foo()
        {
            std::lock_guard<mtx_type> lock(mut);
            // Do something thread safe
        }
    
        void bar()
        {
            std::lock_guard<mtx_type> lock(mut);
            // Do something else thread safe
        }
    
        std::unique_lock<mtx_type> lock() const
        {
            return std::unique_lock<mtx_type>(mut);
        }
    private:
        mutable mtx_type mut;
    };
    
    void blah(Test& obj)
    {
        auto lck = obj.lock();
        obj.foo();
        obj.bar();
    }
    

    Alternativ könnte man das Locking-Zeugs komplett von Test trennen und dafür einen Wrapper benutzen, der einem nur über so ein lock-artiges Objekt den Zugriff auf das innere Objekt erlaubt, also

    void blah(ThreadSafe<Test>& obj)
    {
        auto handle = obj.lock();
        handle->foo();
        handle->bar();
    }
    

    Das geht dann wieder ohne recursive_mutex und ist übrigens ziemlich genau der Ansatz, für den man sich in Rust entschieden hat, weil man so nicht vergessen kann, den Mutex zu "locken".

    Die Implementierung dieses Wrappers ist als Übung dem Leser überlassen. 😉



  • Oder

    void Test::foobar()
    {
        std::lock_guard<mtx_type> lock(mut); // recursive_mutex
        foo();
        bar();
    }
    


  • Ok, ich hab jetzt auf recursive_mutex umgestellt, danke für die Hilfe 👍



  • Ich würde sagen es kommt drauf an ob man auf Performance optimiert oder nicht.
    Wenn nicht, dann kann man die Klasse einfach intern synchronisieren, und weiter nichts machen.
    Wenn der aufrufende Code dann mehrere Operationen ohne "Unterbrechung" auf den Objekten der Klasse durchführen möchte/muss, dann kann er extern nochmal synchronisieren. Also selbst eine eigene Mutex verwenden.

    Wenn man auf Performance optimiert, dann kann man der Klasse einen Mutex-Getter verpassen, der Zugriff auf die "innere" Mutex gibt. (Bzw. so wie von krümelkacker gezeigt eine lock() Funktion die den fertig gelockten unique_lock zurückgibt.) Dann kann man noch für alle Methoden nicht lockende Alternativen machen, ala

    class Test
    {
    public:
    
        void foo()
        {
            std::lock_guard<mtx_type> lock(mut);
            foo(lock);
        }
    
        void foo(std::lock_guard<mtx_type> const& lock)
        {
            // Idealerweise prüft man hier dass lock wirklich gelockt ist und auch
            // wirklich die eigene Mutex gelockt hat (und nicht irgend eine andere)
    
            // do something
        }
    //...
    

    In dem Fall reicht dann auch ne normale Mutex, recursive_mutex ist nicht nötig.


Anmelden zum Antworten