Qual è la vera ragione per cui i blocchi (sentinelle) in OO sono difficili da ragionare? [chiuso]

6

In questo talk , Rich Hickey introduce Clojure. Dà una demo di Ants e parla delle sue motivazioni per l'implementazione di un sistema di memoria transazionale software (STM).

IlsuoragionamentoperSTMèquel"locks are hard to reason about" .

L'ho preso per indicare la situazione di deadlock, in cui due thread stanno cercando di aggiornare lo stesso elemento e conservano le risorse di cui ha bisogno l'altro thread.
Quindi per essere atomico un thread deve tornare indietro e riprovare e l'altro deve rotolare in avanti.

Per fare in modo che funzioni in un linguaggio orientato agli oggetti, devi utilizzare un framework STM o eseguirlo manualmente.

Questa è la mia comprensione, ma ho il sospetto che io abbia trattato solo un caso problematico e immagino ci sia un principio più generale che mi manca. Forse il problema è che i lucchetti non compongono o qualcosa del genere.

La mia domanda è: Qual è la vera ragione per cui i lock (sentinels) in OO sono difficili da ragionare?

    
posta hawkeye 31.10.2014 - 11:40
fonte

6 risposte

16

Non ha nulla a che fare con OO in particolare. I problemi sono problemi con il paradigma del "blocco esplicito" come approccio alla concorrenza, indipendentemente dagli altri paradigmi usati in altre parti del problema.

Non ho visto quel discorso di due ore, quindi non commenterò il ragionamento di Hickey. Ecco le critiche alle serrature che sento più comunemente, riassunte:

  • I lucchetti non sono componibili. Se hai due parti di codice A e B che funzionano correttamente (in particolare, atomicamente) da sole, non puoi scrivere A; B per eseguire insieme le due azioni , atomicamente . Perlomeno non senza né andare in giro con le serrature A e B usare (che sono spesso private, per buone ragioni) o introdurre un altro lucchetto e introdurlo ovunque debba essere rispettato.

  • I lucchetti, come di solito implementati, non sono formalizzati in ciò che proteggono e quando devono essere acquisiti. Se il sistema è valido, verrà documentato, ma i programmatori non riceveranno assistenza automatizzata nell'aderire alle regole.

  • I blocchi sono di basso livello. Per comprendere correttamente le loro interazioni e il loro significato per la sincronizzazione, è necessario considerare tutti i possibili modi in cui le esecuzioni concorrenti potrebbero intercalare e tutti i possibili modi in cui il codice potrebbe essere riordinato. Il ragionamento dipendente dal flusso è già complesso per un singolo thread di esecuzione, ma la concorrenza fa esplodere il numero di percorsi da considerare.

I sostenitori della programmazione funzionale sostengono che OOP necessiti anche di molta più sincronizzazione perché utilizza uno stato mutevole molto più condiviso, ma questo è oltre il punto qui. Quanta sincronizzazione è necessaria è ortogonale al modo in cui hai scelto di sincronizzare.

    
risposta data 31.10.2014 - 12:55
fonte
12

Non è il blocco difficile da capire. I lucchetti sono facili. Funzionano proprio come le serrature nella vita reale. È la concorrenza difficile da capire.

L'unico motivo per cui il blocco fa girare la testa alle persone è che lo usiamo solo in sistemi concorrenti. Ma immaginiamo per un momento di avere un sistema altamente concorrente senza sincronizzazione di sorta; lo stato del sistema cambierà nel tempo in configurazioni esilaranti non valide. Molte di queste configurazioni sono altrettanto difficili da capire; fanno andare le persone "Eh? Come mai potrebbe mai succedere questo?" Il motivo è che non siamo costruiti per comprendere lo strano mix di modifiche simultanee e alternate che i moderni sistemi di time-slicing producono. Poco altro nella vita reale funziona in questo modo, quindi il nostro cervello ha un momento davvero difficile con esso.

    
risposta data 31.10.2014 - 11:57
fonte
6

Non ho alcuna esperienza con STM, ma mi occuperò della parte di blocco.

Ottenere codice a thread singolo è difficile. È abbastanza significativo che la metodologia prevalente sia quella di creare una grande batteria di test unitari e sperare che se tutti passino, nulla è sbagliato. Per un codice che non comporta perdite di vite umane o miliardi di dollari, si tratta di un'approssimazione ragionevole, ma il punto che sto cercando di fare è che il codice che provi sia corretto. Più difficile di quanto la maggior parte delle persone sia disposta a dedicarsi.

Ora considera il codice concorrente utilizzando i blocchi. Per dimostrare che funziona, devi dimostrare di aver sincronizzato manualmente i thread correttamente per ogni possibile interleaving / scheduling . È un compito monumentale. E se si sbaglia, perché il codice non è deterministico, anche il comportamento con bachi è non deterministico! Non è diverso dalla gestione manuale della memoria, tranne per il fatto che le risorse di tracciamento nel codice a thread singolo sono più semplici (ma ancora troppo difficili da ottenere).

Il codice ancora peggiore che utilizza i blocchi non è componibile. Supponiamo di avere due raccolte e di rendere tutte le operazioni su questi thread-safe. Supponiamo ora di voler rimuovere un elemento da una raccolta e aggiungerlo all'altro. Solo perché la rimozione dell'elemento è thread-safe e l'aggiunta dell'elemento è thread-safe, non significa che l'intera operazione sia thread-safe. Se hai scritto il codice per le raccolte thread-safe e il tuo collega è quello che deve scrivere il programma, è su loro per assicurarsi che qualsiasi combinazione di azioni che fanno sia sicura. Contrariamente a quello con lo scenario a thread singolo, dove la dimostrazione che il codice di inserimento e cancellazione è corretto dimostra anche che lo spostamento di un elemento tra le raccolte è corretto. Confrontati anche con la gestione manuale della memoria, in cui la liberazione della memoria allocata è il lavoro del chiamante della funzione.

Non sono proprio sicuro di come OO si adatta a questo. La concorrenza basata sul blocco è difficile, non importa quale paradigma tu scelga.

    
risposta data 31.10.2014 - 12:52
fonte
5

Il particolare problema con OOP e la concorrenza è che spesso i programmatori sono tentati di cercare di nascondere l'implementazione della sicurezza del thread usando l'incapsulamento.

Quando questo viene fatto cuocendo le serrature direttamente negli oggetti, allora è quasi sempre un'astrazione che perde, a causa degli effetti collaterali che bloccano la causa.

  1. Se possiedi molte di queste serrature nascoste, è improbabile che ti imbatti in condizioni di gara, ma diventa davvero facile ottenere deadlock (che probabilmente sono in realtà solo condizioni di gara, ma penso che si ottenga il mio punto). Quindi devi ragionare sull'ordine in cui chiami determinati metodi per evitare i deadlock. Non è facile ragionare. E l'incapsulamento che volevi semplicemente non è lì.

  2. Oppure usi i blocchi più scarsamente, il che rende più facile evitare i deadlock. Ma è anche difficile ragionare su dove posizionare le serrature per evitare comportamenti strani o inaspettati. Ciò significa anche che troppo spesso dovrai accoppiare intimamente l'implementazione di un oggetto a quando (al contrario di solo come ) verrà interfacciato dall'esterno mondo. Quindi non c'è nemmeno un vero incapsulamento.

Quindi non è che avere oggetti rende più difficile gestire la concorrenza. È che cercare di avere a che fare con l'accesso concorrente a oggetti rende più difficile gestire questi oggetti. È sempre possibile, ad esempio, avere un modello che non è il meno sicuro per i thread e garantire solo che tutta l'interazione sia instradata attraverso una facciata che si occupa della sicurezza del filo (ad esempio attraverso un modello di reattore). Quindi la facciata ha la sola responsabilità di gestire la concorrenza.

    
risposta data 31.10.2014 - 12:30
fonte
4

Per comprendere la correttezza di una determinata riga di codice di concorrenza basato su blocco, devi comprendere l'intero programma.

Ora puoi risparmiare un po '. Se riesci a dimostrare a te stesso che tutto ciò a cui si accede in quel bit di codice di concorrenza basato su lock è accessibile solo all'interno di un particolare lock, ora devi solo capire ogni bit di codice nel programma che accede anche a quel lock, il suo stato quando avvia l'accesso di blocco e tutto ciò che fa mentre lo trattiene.

Questo perché la concorrenza basata su lock mantiene solo la "sicurezza del thread" bloccando l'accesso ai dati a un numero limitato di thread contemporaneamente (a volte 1, a volte solo lettori, ecc.). Quindi devi capire tutto il comportamento di blocco e il comportamento durante il blocco, per sapere se il sistema di blocco in realtà sta facendo ciò che vuoi.

Successivamente, poiché i blocchi sono soggetti a deadlock, per comprendere più blocchi devi capire non solo il codice in un blocco, ma ogni sequenza possibile puoi acquisire i blocchi.

E i nostri linguaggi e i nostri sistemi di tipi non aiutano molto con queste attività.

Diventa peggio. Come l'altro difficile da rintracciare e gli errori comuni, i problemi di deadlock e race condition di solito non avvengono in modo affidabile. Puoi scrivere il codice, fare alcuni test semplici, persino eseguirlo in produzione e farlo funzionare quasi sempre. Tranne a volte fallisce in modo spettacolare.

Errori spettacolari davvero rari sono difficili da debugare a prescindere di quanto sia facile ragionare. Qui abbiamo rari e spettacolari guasti difficili da ragionare.

E peggiora. Quando si impara a programmare, le persone si imbattono in una serie di ostacoli. Capire lo stato / l'assegnazione / il flusso di dati è un ostacolo che alcuni non riescono a superare. Comprendere i condizionali e le chiamate di funzione è un altro. La ricorsione e l'iterazione gettano alcune persone per un ciclo. Indiretti e puntatori hanno impedito a un intero gruppo di aspiranti programmatori di diventare utili. Gestione delle risorse. E la concorrenza e la sincronizzazione sono un altro ostacolo.

Possiamo generare più programmatori utili rimuovendo alcuni di questi ostacoli "nel modo" di essere produttivi. Esistono linguaggi che nascondono il riferimento indiretto e i puntatori dal programmatore e gestiscono l'attività di gestione delle risorse per te (memoria) e puoi imparare a diventare un programmatore produttivo in quelle lingue senza superare questo ostacolo.

In quasi ogni lingua puoi diventare un "programmatore produttivo" senza superare l'ostacolo della concorrenza. Puoi produrre codice utile, essere uno sviluppatore professionista, laurearsi dall'università - tutti senza mai ottenere concorrenza.

Quelli che non hanno ottenuto il controllo di flusso, il flusso di dati e l'iterazione non sono mai diventati programmatori professionisti. Quelli che non ottengono concorrenza sono grandi sviluppatori produttivi in molte aziende.

    
risposta data 31.10.2014 - 19:16
fonte
0

Le serrature non sono difficili da capire e anche la concorrenza non è particolarmente difficile da comprendere (praticamente tutto ciò che accade intorno a noi nella vita è concomitante). È la pianificazione dei blocchi che è difficile da capire. Quasi nulla nei blocchi della vita reale, ma nel calcolo sequenziale con un singolo thread tutto fa, e tutto ciò che facciamo dividendo thread o processi è fingere che ce ne siano diversi, quando in realtà ci stiamo bloccando costantemente l'un l'altro. Pianificare quel blocco è davvero difficile da ottenere. È difficile programmare il tempo della CPU per uno, e anche programmare risorse condivise con stato, ma fare entrambi può essere una vera bestia.

Inserisci cose come il runtime di Erlang. La concorrenza è semplice e funziona come ti aspetti. Questo perché il runtime gestisce la pianificazione per te, lasciandoti libero di pensare al problema che stai effettivamente cercando di risolvere. La pianificazione è la grande vittoria, non l'altra roba che viene pubblicizzata in continuazione. Il costo per tutta quella facile concorrenza è che lo stato no può essere condiviso. Sembra che questo non sia qualcosa con cui la maggior parte dei programmatori si sente a proprio agio; ci sono altissime incidenze di persone che fanno "leggere un libro su Erlang e hanno rinunciato" alla danza e si sono lasciati alle spalle dove hanno iniziato.

Qualunque lingua o ambiente tu scelga di risolvere problemi in esso è importante riconoscere che la natura del problema non sono i blocchi, lo stato o la lingua, è la pianificazione dei blocchi ogni volta che lo stato è condiviso.

    
risposta data 31.10.2014 - 17:49
fonte

Leggi altre domande sui tag