std::mutex vs std::recursive_mutex als klaslid

Ik heb gezien dat sommige mensen haten op recursive_mutex:

http://www.zaval.org/resources/library/butenhof1.html

Maar als ik nadenk over het implementeren van een klasse die thread-safe (mutex-beveiligd) is, lijkt het me ondraaglijk moeilijk om te bewijzen dat elke methode die mutex-beveiligd zou moeten zijn, mutex-beveiligd is en dat mutex maximaal één keer vergrendeld is.

Dus voor objectgeoriënteerd ontwerp moet std::recursive_mutexstandaard zijn en std::mutexin het algemeen als prestatie-optimalisatie worden beschouwd, tenzij het slechts in één plaats (om slechts één bron te beschermen)?

Voor alle duidelijkheid: ik heb het over een niet-statische privémutex. Elke klasse-instantie heeft dus maar één mutex.

Aan het begin van elke openbare methode:

{
    std::scoped_lock<std::recursive_mutex> sl;

Antwoord 1, autoriteit 100%

Meestal, als je denkt dat je een recursieve mutex nodig hebt, dan is je ontwerp verkeerd, dus het zou zeker niet de standaard moeten zijn.

Voor een klasse met een enkele mutex die de gegevensleden beschermt, moet de mutex worden vergrendeld in alle public-lidfuncties en moeten alle private-lidfuncties ervan uitgaan de mutex is al vergrendeld.

Als een publiclidfunctie een andere publiclidfunctie moet aanroepen, splits dan de tweede in tweeën: een privateimplementatiefunctie die doet het werk, en een publicledenfunctie die de mutex vergrendelt en de privateaanroept. De eerste lidfunctie kan dan ook de implementatiefunctie aanroepen zonder zich zorgen te hoeven maken over recursieve vergrendeling.

bijv.

class X {
    std::mutex m;
    int data;
    int const max=50;
    void increment_data() {
        if (data >= max)
            throw std::runtime_error("too big");
        ++data;
    }
public:
    X():data(0){}
    int fetch_count() {
        std::lock_guard<std::mutex> guard(m);
        return data;
    }
    void increase_count() {
        std::lock_guard<std::mutex> guard(m);
        increment_data();
    } 
    int increase_count_and_return() {
        std::lock_guard<std::mutex> guard(m);
        increment_data();
        return data;
    } 
};

Dit is natuurlijk een triviaal gekunsteld voorbeeld, maar de functie increment_datawordt gedeeld tussen twee openbare lidfuncties, die elk de mutex vergrendelen. In single-threaded code kan het worden inline gezet in increase_count, en increase_count_and_returnzou dat kunnen noemen, maar dat kunnen we niet doen in multithreaded code.

Dit is slechts een toepassing van goede ontwerpprincipes: de openbare ledenfuncties nemen de verantwoordelijkheid voor het vergrendelen van de mutex en delegeren de verantwoordelijkheid voor het doen van het werk aan de privéledenfunctie.

Dit heeft het voordeel dat de publiclidfuncties alleen hoeven te worden aangeroepen als de klasse in een consistente staat is: de mutex is ontgrendeld, en als deze eenmaal is vergrendeld, houden alle invarianten stand. Als je publiclidfuncties van elkaar oproept, dan moeten ze het geval afhandelen dat de mutex al is vergrendeld en dat de invarianten niet noodzakelijkerwijs gelden.

Het betekent ook dat zaken als wachttijden voor voorwaardevariabelen zullen werken: als u een slot op een recursieve mutex doorgeeft aan een voorwaardevariabele, dan (a) moet u std::condition_variable_anygebruiken omdat std::condition_variablezal niet werken, en (b) slechts één niveau van vergrendeling is vrijgegeven, dus u kunt nog steeds de vergrendeling vasthouden, en dus een impasse omdat de thread die het predikaat zou activeren en de melding zou doen dat niet kan verkrijg het slot.

Ik heb moeite om een ​​scenario te bedenken waarin een recursieve mutex vereist is.


Antwoord 2, autoriteit 29%

moet std::recursive_mutexstandaard zijn en std::mutexworden beschouwd als prestatie-optimalisatie?

Niet echt, nee. Het voordeel van het gebruik van niet-recursieve vergrendelingen is nietalleen een prestatie-optimalisatie, het betekent dat uw code zichzelf controleert of atomaire bewerkingen op bladniveau echt bladniveau zijn, ze roepen niet iets anders aan die het slot gebruikt.

Er is een redelijk veelvoorkomende situatie waarin u:

  • een functie die een bewerking implementeert die moet worden geserialiseerd, dus het neemt de mutex en doet het.
  • een andere functie die een grotere geserialiseerde bewerking implementeert en de eerste functie wil aanroepen om er één stap van uit te voeren, terwijl deze de vergrendeling voor de grotere bewerking vasthoudt.

Om een ​​concreet voorbeeld te geven: misschien verwijdert de eerste functie atomair een knooppunt uit een lijst, terwijl de tweede functie atomair tweeknooppunten uit een lijst verwijdert (en u wilt nooit dat een andere thread de lijst met slechts één van de twee knooppunten verwijderd).

Hiervoor heb je geen nodigrecursieve mutexen. U kunt bijvoorbeeld de eerste functie herschikken als een openbare functie die de vergrendeling overneemt en een privéfunctie aanroept die de bewerking “onveilig” uitvoert. De tweede functie kan dan dezelfde privéfunctie aanroepen.

Soms is het echter handig om in plaats daarvan een recursieve mutex te gebruiken. Er is nog steeds een probleem met dit ontwerp: remove_two_nodesroept remove_one_nodeaan op een punt waar een klasse-invariant niet geldt (de tweede keer dat deze wordt aangeroepen, bevindt de lijst zich precies in de staat die we niet willen onthullen). Maar in de veronderstelling dat we weten dat remove_one_nodeniet op die invariant vertrouwt, is dit geen geweldige fout in het ontwerp, het is gewoon dat we onze regels iets complexer hebben gemaakt dan de ideale “all class invarianten zijn altijd geldig wanneer een openbare functie wordt ingevoerd”.

Dus, de truc is af en toe handig en ik heb niet zo’n hekel aan recursieve mutexen in die mate dat artikel dat doet. Ik heb niet de historische kennis om te beweren dat de reden voor hun opname in Posix anders is dan wat het artikel zegt, “om mutex-attributen en thread-extensies aan te tonen”. Ik beschouw ze echter zeker niet als de standaard.

Ik denk dat het veilig is om te zeggen dat als je in je ontwerp niet zeker weet of je een recursief slot nodig hebt of niet, je ontwerp onvolledig is. Je zult later spijt krijgen van het feit dat je code schrijft en je niet weetiets dat zo fundamenteel belangrijk is als of het slot al vastgehouden mag worden of niet. Plaats dus geen recursief slot “voor het geval dat”.

Als je weet dat je er een nodig hebt, gebruik er dan een. Als je weet dat je er geen nodig hebt, dan is het gebruik van een niet-recursief slot niet alleen een optimalisatie, het helpt ook om een ​​beperking van het ontwerp af te dwingen. Het is nuttiger dat het tweede slot faalt, dan dat het slaagt en het feit verbergt dat je per ongeluk iets hebt gedaan waarvan je ontwerp zegt dat het nooit zou mogen gebeuren. Maar als je je ontwerp volgt en de mutex nooit dubbel vergrendelt, kom je er nooit achter of het recursief is of niet, en dus is een recursieve mutex niet directschadelijk.

Deze analogie kan mislukken, maar hier is een andere manier om ernaar te kijken. Stel je voor dat je de keuze had tussen twee soorten aanwijzers: een die het programma afbreekt met een stacktrace wanneer je een null-aanwijzer derefeert, en een andere die 0retourneert (of om het uit te breiden naar meer typen: gedraagt ​​zich als als de aanwijzer verwijst naar een object met geïnitialiseerde waarde). Een niet-recursieve mutex is een beetje zoals degene die afbreekt, en een recursieve mutex is een beetje zoals degene die 0 retourneert. Ze hebben allebei potentieel hun gebruik — mensen doen soms wat moeite om een ​​”stille niet-een -waarde” waarde. Maar in het geval dat uw code is ontworpen om nooit een null-pointer te derefereren, wilt u niet standaardde versie gebruiken die dat stilletjes toestaat.


Antwoord 3, autoriteit 9%

Ik ga niet direct in op het debat over mutex versus recursive_mutex, maar ik dacht dat het goed zou zijn om een ​​scenario te delen waarin recursive_mutex’en absoluut cruciaal zijn voor het ontwerp.

Bij het werken met Boost :: Asio, boost :: coroutine (en waarschijnlijk dingen als NT-vezels, hoewel ik minder bekend met hen ben), is het absoluut essentieel dat uw mutexen recursief zijn, zelfs zonder het ontwerpprobleem van re-entrancy .

De reden is omdat de Coroutine-gebaseerde aanpak door het ontwerp van zijn ontwerp de uitvoering binnenkant een routine zal opschorten en vervolgens vervolgens hervat. Dit betekent dat twee methoden voor het hoogste niveau van een klasse kunnen “worden geroepen op hetzelfde moment op dezelfde draad” zonder subopdrachten worden gemaakt.

Other episodes