Perché non riesco a verificare se un mutex è bloccato?


28

C ++ 14 sembra aver omesso un meccanismo per verificare se un std::mutex è bloccato o meno. Vedi questa domanda SO:

/programming/21892934/how-to-assert-if-a-stdmutex-is-locked

Ci sono molti modi per aggirare questo, ad esempio usando;

std::mutex::try_lock()
std::unique_lock::owns_lock()

Ma nessuna di queste sono soluzioni particolarmente soddisfacenti.

try_lock()è autorizzato a restituire un falso negativo e ha un comportamento indefinito se il thread corrente ha bloccato il mutex. Ha anche effetti collaterali. owns_lock()richiede la costruzione di un unique_locksopra l'originale std::mutex.

Ovviamente potrei farlo da solo, ma preferirei capire le motivazioni per l'interfaccia attuale.

La capacità di controllare lo stato di un mutex (ad es. std::mutex::is_locked()) Non mi sembra una richiesta esoterica, quindi sospetto che il Comitato Standard abbia deliberatamente omesso questa funzionalità piuttosto che essere una svista.

Perché?

Modifica: Ok, quindi forse questo caso d'uso non è così comune come mi aspettavo, quindi illustrerò il mio scenario particolare. Ho un algoritmo di apprendimento automatico che è distribuito su più thread. Ogni thread funziona in modo asincrono e torna a un pool principale dopo aver completato un problema di ottimizzazione.

Quindi blocca un mutex master. Il thread deve quindi scegliere un nuovo genitore da cui mutare una prole, ma può scegliere solo da genitori che attualmente non hanno una prole ottimizzata da altri thread. Devo quindi eseguire una ricerca per trovare genitori che non sono attualmente bloccati da un altro thread. Non vi è alcun rischio che lo stato del mutex cambi durante la ricerca, poiché il thread principale mutex è bloccato. Ovviamente ci sono altre soluzioni (attualmente sto usando un flag booleano) ma ho pensato che il mutex offre una soluzione logica a questo problema, poiché esiste ai fini della sincronizzazione tra thread.


42
Non puoi davvero ragionevolmente verificare se un mutex è bloccato, perché un nanosecondo dopo il controllo può essere sbloccato o bloccato. Quindi se hai scritto "if (mutex_is_locked ()) ..." allora mutex_is_locked potrebbe restituire il risultato corretto, ma quando "if" viene eseguito, è sbagliato.
gnasher729,

1
Questo ^. Quali informazioni utili speri di ottenere is_locked?
Inutile

3
Sembra un problema XY. Perché stai cercando di impedire il riutilizzo dei genitori solo mentre viene generato un bambino? Hai l'obbligo che un genitore possa avere esattamente una sola prole? Il tuo lucchetto non lo impedirà. Non hai generazioni chiare? In caso contrario, sei consapevole che le persone che possono essere ottimizzate più velocemente hanno una forma fisica più elevata, dal momento che possono essere selezionate più spesso / prima? Se usi generazioni, perché non selezioni tutti i genitori in anticipo, quindi lascia che i thread recuperino i genitori da una coda? Generare la prole è davvero così costoso da aver bisogno di più thread?
amon,

10
@quant - Non vedo perché i mutex dell'oggetto genitore nella tua applicazione di esempio debbano essere mutex: se hai un mutex master che viene bloccato ogni volta che sono impostati, puoi semplicemente usare una variabile booleana per indicarne lo stato.
Periata Breatta,

4
Non sono d'accordo con l'ultima frase della domanda. Un semplice valore booleano è molto più pulito di un mutex qui. Trasformalo in un bool atomico se non vuoi bloccare il mutex principale per "restituire" un genitore.
Sebastian Redl,

Risposte:


53

Riesco a vedere almeno due gravi problemi con l'operazione suggerita.

Il primo è già stato menzionato in un commento di @ gnasher729 :

Non puoi davvero ragionevolmente verificare se un mutex è bloccato, perché un nanosecondo dopo il controllo può essere sbloccato o bloccato. Quindi, se hai scritto if (mutex_is_locked ()) …, allora mutex_is_lockedpotrebbe tornare il risultato corretto, ma per il momento la ifsi esegue, è sbagliato.

L'unico modo per essere sicuri che la proprietà "è attualmente bloccata" di un mutex non cambi è, bene, bloccarlo da soli.

Il secondo problema che vedo è che, a meno che non si blocchi un mutex, il thread non si sincronizza con il thread che aveva precedentemente bloccato il mutex. Pertanto, non è nemmeno ben definito parlare di "prima" e "dopo" e se il mutex è bloccato o meno è una specie di domanda se il gatto di Schrödiger è attualmente vivo senza tentare di aprire la scatola.

Se ho capito bene, entrambi i problemi sarebbero discutibili nel tuo caso particolare grazie al blocco del master mutex. Ma questo non mi sembra un caso particolarmente comune, quindi penso che il comitato abbia fatto la cosa giusta non aggiungendo una funzione che potrebbe essere in qualche modo utile in scenari molto speciali e causare danni in tutti gli altri. (Nello spirito di: "Rendi le interfacce facili da usare correttamente e difficili da usare in modo errato.")

E se posso dire, penso che l'installazione che hai attualmente non sia la più elegante e potrebbe essere riformulata per evitare del tutto il problema. Ad esempio, invece del thread principale che controlla tutti i potenziali genitori per uno che non è attualmente bloccato, perché non mantenere una coda di genitori pronti? Se un thread vuole ottimizzarne un altro, fa uscire quello successivo dalla coda e non appena ha nuovi genitori, li aggiunge alla coda. In questo modo, non è nemmeno necessario il thread principale come coordinatore.


Grazie, questa è una buona risposta. Il motivo per cui non voglio mantenere una fila di genitori pronti è che devo preservare l'ordine in cui i genitori sono stati creati (poiché ciò determina la loro durata di vita). Questo può essere fatto facilmente con una coda LIFO. Se comincio a tirare dentro e fuori le cose dovrei mantenere un meccanismo di ordinamento separato che complicherebbe le cose, da qui l'approccio attuale.
quant

14
@quant: se hai due scopi per mettere in coda i genitori, puoi farlo con due code ....

@quant: stai eliminando un elemento (al massimo) una volta, ma presumibilmente stai elaborando ogni volta più volte, quindi stai ottimizzando il raro caso a spese del caso comune. Questo è raramente desiderabile.
Jerry Coffin,

2
Ma è ragionevole chiedersi se il thread corrente ha bloccato il mutex.
Espiazione limitata

@LimitedAtonement Non proprio. Per fare questo mutex deve memorizzare ulteriori informazioni (ID thread), rendendolo più lento. I mutex ricorsivi lo fanno già, dovresti invece.
StaceyGirl,

9

Sembra che tu stia utilizzando i mutex secondari non per bloccare l'accesso a un problema di ottimizzazione, ma per determinare se un problema di ottimizzazione è stato ottimizzato in questo momento o meno.

Questo è completamente inutile. Avrei un elenco di problemi che devono essere ottimizzati, un elenco di problemi da ottimizzare in questo momento e un elenco di problemi che sono stati ottimizzati. (Non prendere letteralmente "elenco", significa "qualsiasi struttura di dati appropriata".

Le operazioni di aggiunta di un nuovo problema all'elenco di problemi non ottimizzati, o di spostamento di un problema da un elenco all'altro, verrebbero eseguite sotto la protezione del singolo mutex "master".


1
Non pensi che un oggetto di tipo std::mutexsia appropriato per una tale struttura di dati?
quant

2
@quant - no. std::mutexsi basa su un'implementazione di mutex definita dal sistema operativo che potrebbe richiedere risorse (ad esempio handle) che sono limitate e lente da allocare e / o su cui operare. L'uso di un singolo mutex per bloccare l'accesso a una struttura di dati interna è probabilmente molto più efficiente e forse anche più scalabile.
Periata Breatta,

1
Considera anche le variabili di condizione. Possono rendere davvero semplici molte strutture di dati come questa.
Cort Ammon - Ripristina Monica il

2

Come altri hanno detto, non c'è nessun caso d'uso in cui is_lockedun mutex sia di alcun beneficio, ecco perché la funzione non esiste.

Il caso con cui stai riscontrando un problema è incredibilmente comune, è fondamentalmente ciò che fanno i thread di lavoro, che sono una delle, se non la più comune, implementazione di thread.

Hai uno scaffale con 10 scatole su di esso. Hai 4 lavoratori che lavorano con queste scatole. Come ti assicuri che i 4 lavoratori lavorino su scatole diverse? Il primo lavoratore prende una scatola dallo scaffale prima di iniziare a lavorarci. Il secondo lavoratore vede 9 scatole sullo scaffale.

Non ci sono mutex per bloccare le scatole, quindi vedere lo stato del mutex immaginario sulla scatola non è necessario e abusare di un mutex come booleano è semplicemente sbagliato. Il mutex blocca lo scaffale.


1

Oltre ai due motivi indicati nella risposta 5gon12eder sopra, vorrei aggiungere che non è né necessario né desiderabile.

Se stai già tenendo un mutex, allora farai meglio a sapere che lo stai trattenendo! Non è necessario chiedere. Proprio come se possiedi un blocco di memoria o qualsiasi altra risorsa, dovresti sapere esattamente se lo possiedi e quando è appropriato rilasciare / eliminare la risorsa.
In caso contrario, il programma è stato progettato in modo errato e si stanno verificando problemi.

Se è necessario accedere alla risorsa condivisa protetta dal mutex e non si è già in possesso del mutex, è necessario acquisire il mutex. Non c'è altra opzione, altrimenti la logica del programma non è corretta.
È possibile che il blocco sia accettabile o inaccettabile, in entrambi i casi lock()o try_lock()darà il comportamento desiderato. Tutto quello che devi sapere, positivamente e senza dubbio, è se hai acquisito con successo il mutex (il valore di ritorno di try_lockti dice). Non ha importanza se qualcun altro lo detiene o se hai un fallimento spurio.

In tutti gli altri casi, senza mezzi termini, non sono affari tuoi. Non hai bisogno di sapere, e non dovresti sapere, o fare ipotesi (per i problemi di tempestività e sincronizzazione menzionati nell'altra domanda).


1
Cosa succede se desidero eseguire un'operazione di classificazione sulle risorse attualmente disponibili per il blocco?
quant

Ma è qualcosa che è realistico per accadere? Lo riterrei piuttosto insolito. Direi che entrambe le risorse hanno già un qualche tipo di classificazione intrinseca, quindi dovrai prima fare (acquisire il blocco per) il più importante. Esempio: è necessario aggiornare la simulazione fisica prima del rendering. Oppure, la classifica è più o meno deliberata, quindi puoi anche try_lockla prima risorsa, e se quella fallisce, prova la seconda. Esempio: tre connessioni persistenti e in pool al server di database, ed è necessario utilizzarne una per inviare un comando.
Damon,

4
@quant - "un'operazione di classificazione delle risorse che sono attualmente disponibili per il blocco" - in generale, fare questo tipo di cose è un modo davvero semplice e veloce per scrivere codice che si blocca in un modo in cui si fatica a capire. Rendere deterministica l' acquisizione e il rilascio di blocchi è, in quasi tutti i casi, la migliore politica. La ricerca di un blocco basato su un criterio che potrebbe cambiare richiede problemi.
Periata Breatta,

@PeriataBreatta Il mio programma è intenzionalmente indeterminato. Vedo ora che questo attributo non è comune, quindi posso capire che l'omissione di funzioni del genere is_locked()potrebbe facilitare tale comportamento.
quant

La classificazione e il blocco di @quant sono problemi completamente separati. Se si desidera in qualche modo ordinare o riordinare una coda con un lucchetto, bloccarlo, ordinarlo, quindi sbloccarlo. Se hai bisogno is_locked, allora esiste una soluzione molto migliore al tuo problema rispetto a quella che hai in mente.
Peter - Unban Robert Harvey,

1

È possibile che si desideri utilizzare atomic_flag con l'ordine di memoria predefinito. Non ha gare di dati e non genera mai eccezioni come fa mutex con più chiamate di sblocco (e si interrompe in modo incontrollabile, potrei aggiungere ...). In alternativa, esiste atomico (ad esempio atomico [bool] o atomico [int] (con parentesi triangolari, non [])), che ha funzioni piacevoli come load e compare_exchange_strong.


1

Voglio aggiungere un caso d'uso per questo: consentirebbe a una funzione interna di garantire come condizione preliminare / asserzione che il chiamante sta effettivamente tenendo il lucchetto.

Per le classi con molte di tali funzioni interne, e forse molte funzioni pubbliche che le chiamano, si potrebbe garantire che qualcuno che aggiunge un'altra funzione pubblica che chiama quella interna abbia effettivamente acquisito il blocco.

class SynchronizedClass
{

public:

void publicFunc()
{
  std::lock_guard<std::mutex>(_mutex);

  internalFuncA();
}

// A lot of code

void newPublicFunc()
{
  internalFuncA(); // whops, forgot to acquire the lock
}


private:

void internalFuncA()
{
  assert(_mutex.is_locked_by_this_thread());

  doStuffWithLockedResource();
}

};
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.