Condizioni di gara nelle lingue JVM rispetto a C / C ++

0

Stavo pensando ai problemi di sincronizzazione dei thread in linguaggi compilati come C ++, rispetto ai problemi di sincronizzazione in linguaggi come Java.

Mi chiedo come un linguaggio JVM come Java non (almeno nella pratica) soffra di coredumps / segfaults / comportamento indefinito quando si verificano condizioni di competizione.

Considera un programma C ++ con 2 thread. Supponiamo che ogni thread condivida un riferimento a un std::vector<int> . Ogni thread scorre continuamente e chiama std::vector<int>::push_back() - con la sincronizzazione no , nessun blocco di sorta. In questo scenario, è molto probabile che il programma segusterà immediatamente, almeno su qualsiasi piattaforma / compilatore mainstream. Il motivo è ovvio: se un thread attiva una riallocazione tramite una chiamata a push_back e l'altro thread finisce per scrivere sul vecchio buffer di memoria libero, è probabile che il programma esegua immediatamente il dump di core. Inoltre, i valori vettoriali interni size e capacity potrebbero essere danneggiati, causando tutti i tipi di comportamento non definito e arresto anomalo.

Ma questo non sembra accadere in Java. Dato lo stesso scenario, in cui si hanno due thread, ciascuno con un riferimento ad una struttura di dati non sincronizzata (come ArrayList<Integer>) , e ogni thread chiama ArrayList<Integer>.add() , il peggiore che sembra accadere in pratica è che viene lanciata un'eccezione, probabilmente un'eccezione ArrayIndexOutofBounds - e, naturalmente, l'ordine di inserimento è totalmente casuale.

Mi rendo conto che la JVM sta eseguendo solo il codice byte (tranne quando sta eseguendo il codice JIT), ma alla fine quel codice byte o codice macchina JIT deve interagire con la memoria di sistema effettiva. Presumo che Java controlli automaticamente gli indici di array con ogni accesso e che la semantica del linguaggio e il garbage collector garantiscano che tutti i riferimenti siano non-penzolanti, ma con una struttura di dati non sincronizzata, quando la memoria potrebbe letteralmente essere tirata fuori dai piedi del programma da un altro thread, in qualsiasi momento, in che modo la JVM non esegue il segfault in questo scenario a meno che non sia in qualche modo in grado di sincronizzare le letture / scritture di memoria?

    
posta Siler 11.11.2014 - 13:50
fonte

5 risposte

2

Java implementa il Modello di memoria Java che stabilisce cosa dovrebbe accadere in determinate situazioni e il nucleo di dumping non è menzionato da nessuna parte in questo documento. Quindi qualsiasi implementazione Java deve fare attenzione ad implementare ciò che dice il modello di memoria, e quindi non è permesso lanciare core. A volte capita, anche se raramente, ma solo a seguito di bug nell'implementazione.

Il modo in cui una particolare implementazione raggiunge l'aderenza alle regole del modello di memoria è lasciata all'implementatore - è solo il comportamento finale che conta. Altri hanno già detto che Garbage Collector svolge un ruolo importante qui e che, in particolare, se si hanno riferimenti a un oggetto in qualsiasi thread, quell'oggetto non può essere liberato dalla memoria.

    
risposta data 11.11.2014 - 16:58
fonte
4

Considera gli effetti che le condizioni di competizione possono avere sull'hardware reale, non nella teoria del comportamento non definito del C ++.

Al livello alto, si ottiene un ordine di inserzione imprevedibile, eventualmente inserimenti persi o addirittura strutture di dati danneggiate. È possibile ottenere completamente questi in Java. (Se esegui inserimenti / rimozioni di corse su una LinkedList abbastanza a lungo, alla fine avrai probabilmente link next / prev incoerenti sui nodi che portano a sequenze di attraversamento in avanti e all'indietro diverse.) Non portano a crash perché sono alti livello di comportamento. Ma porteranno a varie eccezioni o semplicemente a dati inaspettati.

A metà livello, i puntatori vengono cambiati sotto i tuoi piedi, gli indici non aggiornati, ecc. La semantica di Java garantisce che questi vengano catturati in modo pulito e convertiti in eccezioni. I puntatori nulli vengono sempre controllati, gli indici vengono sempre controllati e il garbage collector rende impossibili i puntatori penzolanti. (Il garbage collector è implementato in modo completamente sicuro per i thread. Non importa quali condizioni di gara scrive il programmatore, non può interferire con il garbage collector, perché la sincronizzazione non è visibile al programmatore.) Questi controlli sono scritti in un modo che impedisce alle condizioni della competizione di eluderle: ad esempio, gli array in Java non possono cambiare di lunghezza e non possono essere sostituiti o invalidati mentre si tiene un riferimento (GC di nuovo), quindi se il compilatore crea solo un riferimento locale garantito-thread prima controllando l'indice, non ci può essere alcuna condizione di competizione tra il controllo dell'indice e l'effettivo accesso.

A basso livello, ottieni le cose davvero strane: letture e scritture lacerate, valori fuori dal nulla, in cui potrebbero apparire valori di puntatori completamente non validi: il tipo di cose che motiva gli specificatori C ++ per dire che qualsiasi razza condizione corrompe il programma ora e per sempre, e anche indietro nel tempo. Java è implementato in un modo che almeno impedisce questa roba davvero strana. Le letture e le scritture delle parole della macchina sono allineate e, se l'architettura è abbastanza voluminosa, probabilmente implementata in un modo più sicuro rispetto al tipico accesso al C ++, al costo di alcune prestazioni. Prendendo questa precauzione, Java può evitare arresti anomali al livello più basso.

Infine, hai il livello del compilatore, in cui il compilatore ottimizzerà il tuo codice in base al presupposto che le condizioni di gara non avvengano. Questa è una buona fonte di strani errori in C ++ anche su architetture molto coerenti come x64, e un buon controargomento per coloro che affermano che "questo codice è perfettamente sicuro, so come funziona la CPU". I compilatori Java semplicemente non eseguono tali ottimizzazioni laddove potrebbero causare arresti anomali. In particolare, la gestione speciale dei limiti di array che ho descritto sopra potrebbe essere ottimizzata se si presuppone che le condizioni di gara non si verifichino. Letture ripetute dallo stesso puntatore, se il puntatore è condiviso ma non sincronizzato, non sono necessari controlli null ripetuti, se si assume che le condizioni di gara non avvengano. I compilatori Java non fanno questa ipotesi e controllano ripetutamente null o creano una copia non condivisa del puntatore, che trasforma la pericolosa condizione di gara segfault in un "innocuo" (non si blocca, produce solo comportamenti strani) "perché la mia scrittura non ha alcun effetto? " condizioni di gara.

Per riassumere, Java esegue alcune operazioni di basso livello per evitare arresti anomali a scapito di prestazioni ridotte. Le cose strane di alto livello possono ancora accadere, ma molto spesso si tradurranno in qualche tipo di eccezione e di solito è più facile eseguire il debug.

    
risposta data 11.11.2014 - 15:32
fonte
2

In std::vector quando avviene una riallocazione il vecchio array è esplicitamente deleted , in java.util.ArrayList invece, il vecchio array è lasciato alla garbage collection.

E il GC è abbastanza conservativo che il riferimento dell'altro thread all'array impedirà che venga ripulito.

but when memory could literally be pulled out from under the program's feet by another thread, at any time, how is Java not segfaulting in this scenario?

Quando qualsiasi thread può ancora fare riferimento a un blocco di memoria non viene raccolto. In altre parole, la memoria non viene mai estratta da nessun thread.

    
risposta data 11.11.2014 - 13:59
fonte
1

È una questione di preferenza. I dump di core possono avvenire solo in un runtime di linguaggio (implementato correttamente) se si invoca un comportamento non definito, poiché non sono mai desiderati; e i progettisti di Java hanno fatto di tutto per eliminare tutti i comportamenti non definiti dalla lingua.

L'atteggiamento della comunità C ++ tende ad essere "Dobbiamo rendere possibile il raggiungimento di qualsiasi cosa chiunque voglia, per le persone che sanno quello che stanno facendo, ma se qualcuno commette un errore, questo è il loro problema". Java ha avuto molti più anni di senno di poi, e gli inventori hanno deciso che, in media, le persone sono meglio servite con un linguaggio che non consente alcune cose e rende altre cose un po 'più lente di quanto sarebbero in modo ottimale, al fine di bandire completamente il runtime problemi causati da un comportamento indefinito. Questo significa controlli prima dell'accesso agli array, dereferenziazione dei puntatori, garbage collection ecc., Ma nel complesso la decisione è stata ripagata, dal momento che i computer sono diventati sempre più veloci ed economici, mentre i programmatori sono costosi per un datore di lavoro come sempre.

    
risposta data 11.11.2014 - 14:04
fonte
0

La risposta di @ratch freak è corretta, ma il motivo del segfault è probabilmente leggermente diverso.

std::vector conserva i suoi valori al posto . Nel caso di std::vector<int> ciò significa semplicemente che contiene una matrice di int s. Per oggetti vuol dire che usa% di conteggio in% co_de.

Quindi, in caso di riallocazione dell'array interno, qualsiasi riferimento estraneo all'array originale o ai dati in esso farà riferimento a new 'd memoria, quindi è probabile che si verifichi un arresto anomalo.

In Java, un free manterrà sempre i riferimenti a ArrayList s i puntatori i.e. Quindi, anche se l'implementazione ha copiato il suo array interno, c'è una buona probabilità che il riferimento che si tiene sarà ancora nel nuovo array.

In altre parole, potresti rendere meno frequente il segfault C ++ se hai usato Object . Questa non è una raccomandazione btw. Ora non è solo una cattiva idea che potrebbe essere un'idea lenta e cattiva.

Il punto debole di @ratchet è più sottile. Anche se std::vector<int *> non contiene alcun riferimento al suo vecchio array interno dopo la riassegnazione, è possibile farlo accidentalmente. In C ++ questo probabilmente sarà un problema. In Java, il GC sa che hai un riferimento, anche se non volevi, e non libererà l'array anche se ora non appartiene più a ArrayList . Di conseguenza, il comportamento sarà imprevedibile ma probabilmente non segfault.

    
risposta data 11.11.2014 - 15:48
fonte

Leggi altre domande sui tag