Come può la sicurezza dei thread essere fornita da un linguaggio di programmazione simile al modo in cui la sicurezza della memoria è fornita da Java e C #?


10

Java e C # garantiscono la sicurezza della memoria controllando i limiti dell'array e le dereferenze dei puntatori.

Quali meccanismi potrebbero essere implementati in un linguaggio di programmazione per prevenire la possibilità di condizioni di gara e deadlock?


3
Potresti essere interessato a quello che fa Rust: concorrenza senza paura con Rust
Vincent Savard,

2
Rendi tutto immutabile o rendi tutto asincrono con canali sicuri. Potresti anche essere interessato a Go ed Erlang .
Theraot,

@Theraot "rende tutto asincrono con canali sicuri" - vorrei che tu potessi approfondirlo.
Mrpyo,

2
@mrpyo non esporresti processi o thread, ogni chiamata è una promessa, tutto viene eseguito contemporaneamente (con il runtime che pianifica la loro esecuzione e creando / raggruppando i thread di sistema dietro le quinte secondo necessità) e la logica che protegge lo stato è nei meccanismi che trasmette informazioni ... il runtime può serializzare automaticamente programmando, e ci sarebbe una libreria standard con soluzione thread-safe per comportamenti più sfumati, in particolare produttore / consumatore e sono necessarie aggregazioni.
Theraot,

2
A proposito, esiste un altro approccio possibile: la memoria transazionale .
Theraot,

Risposte:


14

Le razze si verificano quando si ha un aliasing simultaneo di un oggetto e, almeno, uno degli alias sta mutando.

Quindi, per prevenire le razze, devi rendere falsa una o più di queste condizioni.

Vari approcci affrontano vari aspetti. La programmazione funzionale sottolinea l'immutabilità che rimuove la mutabilità. Il blocco / atomica rimuove la simultaneità. I tipi affini rimuovono l'aliasing (Rust rimuove l'aliasing mutabile). I modelli di attori di solito rimuovono l'aliasing.

È possibile limitare gli oggetti che possono essere aliasati in modo che sia più facile garantire che le condizioni di cui sopra siano evitate. È qui che entrano in gioco i canali e / o gli stili di passaggio dei messaggi. Non puoi alias la memoria arbitraria, ma solo la fine di un canale o di una coda che è organizzata per essere libera da corse. Di solito evitando la simultaneità, cioè blocchi o atomica.

Il rovescio della medaglia di questi vari meccanismi è che limitano i programmi che è possibile scrivere. Maggiore è la limitazione, minore è il numero di programmi. Quindi nessun alias o nessuna mutabilità funzionano, e sono facili da ragionare, ma sono molto limitanti.

Ecco perché la ruggine sta suscitando tanto scalpore. È un linguaggio ingegneristico (rispetto a quello accademico) che supporta l'aliasing e la mutabilità ma ha il compilatore che verifica che non si verifichino contemporaneamente. Sebbene non sia l'ideale, consente una più ampia classe di programmi da scrivere in modo sicuro rispetto a molti dei suoi predecessori.


11

Java e C # garantiscono la sicurezza della memoria controllando i limiti dell'array e le dereferenze dei puntatori.

È importante innanzitutto pensare a come C # e Java lo fanno. Lo fanno convertendo ciò che è un comportamento indefinito in C o C ++ in un comportamento definito: crash del programma . Dereferenze nulle ed eccezioni all'indice di array non dovrebbero mai essere rilevate in un programma C # o Java corretto; non dovrebbero accadere in primo luogo perché il programma non dovrebbe avere quel bug.

Ma non credo che cosa intendi con la tua domanda! Potremmo facilmente scrivere un runtime "deadlock safe" che controlla periodicamente per vedere se ci sono n thread che si aspettano reciprocamente e terminano il programma se ciò accade, ma non penso che ti soddisferebbe.

Quali meccanismi potrebbero essere implementati in un linguaggio di programmazione per prevenire la possibilità di condizioni di gara e deadlock?

Il prossimo problema che dovremo affrontare con la tua domanda è che le "condizioni di gara", a differenza dei deadlock, sono difficili da rilevare. Ricorda, ciò che stiamo cercando nella sicurezza del thread non è eliminare le gare . Ciò che stiamo cercando è rendere il programma corretto, indipendentemente da chi vince la gara ! Il problema con le condizioni di gara non è che due thread girano in un ordine indefinito e non sappiamo chi finirà per primo. Il problema con le condizioni di gara è che gli sviluppatori dimenticano che alcuni ordini di finitura dei thread sono possibili e non tengono conto di tale possibilità.

Quindi la tua domanda si riduce sostanzialmente a "c'è un modo in cui un linguaggio di programmazione può garantire che il mio programma sia corretto?" e la risposta a questa domanda è, in pratica, no.

Finora ho solo criticato la tua domanda. Vorrei provare a cambiare marcia qui e rivolgermi allo spirito della tua domanda. Ci sono scelte che i designer linguistici potrebbero fare per mitigare la terribile situazione in cui ci troviamo con il multithreading?

La situazione è davvero orribile! Ottenere il codice multithread corretto, in particolare su architetture di modelli di memoria deboli, è molto, molto difficile. È istruttivo pensare al motivo per cui è difficile:

  • Più thread di controllo in un processo sono difficili da ragionare. Un thread è abbastanza difficile!
  • Le astrazioni diventano estremamente trapelate in un mondo multithread. Nel mondo a thread singolo ci viene garantito che i programmi si comportano come se fossero eseguiti in ordine, anche se non sono effettivamente eseguiti in ordine. Nel mondo multithread, non è più così; le ottimizzazioni che sarebbero invisibili su un singolo thread diventano visibili e ora lo sviluppatore deve capire quelle possibili ottimizzazioni.
  • Ma peggiora. La specifica C # afferma che un'implementazione NON è richiesta per avere un ordine coerente di letture e scritture che può essere concordato da tutti i thread . L'idea che ci siano "gare" e che ci sia un chiaro vincitore, in realtà non è vera! Considera una situazione in cui ci sono due scritture e due letture per alcune variabili su molti thread. In un mondo sensibile potremmo pensare "bene, non possiamo sapere chi vincerà le gare, ma almeno ci sarà una gara e qualcuno vincerà". Non siamo in quel mondo sensibile. C # consente a più thread di non essere d' accordo sull'ordinamento in cui avvengono le letture e le scritture; non c'è necessariamente un mondo coerente che tutti osservano.

Quindi c'è un modo ovvio in cui i progettisti del linguaggio possono migliorare le cose. Abbandona le vittorie sulle prestazioni dei moderni processori . Rendi tutti i programmi, anche quelli multi-thread, con un modello di memoria estremamente potente. Questo renderà i programmi multithread molto, molte volte più lenti, il che funziona direttamente contro la ragione per avere programmi multithread in primo luogo: per migliorare le prestazioni.

Anche lasciando da parte il modello di memoria, ci sono altri motivi per cui il multithreading è difficile:

  • La prevenzione di deadlock richiede l'analisi dell'intero programma; è necessario conoscere l'ordine globale in cui è possibile rimuovere i blocchi e applicare tale ordine in tutto il programma, anche se il programma è composto da componenti scritti in momenti diversi da organizzazioni diverse.
  • Lo strumento principale che ti diamo per domare il multithreading è il blocco, ma i blocchi non possono essere composti .

Quest'ultimo punto porta ulteriori spiegazioni. Per "compostabile" intendo quanto segue:

Supponiamo di voler calcolare un int con un doppio. Scriviamo una corretta implementazione del calcolo:

int F(double x) { correct implementation here }

Supponiamo di voler calcolare una stringa data un int:

string G(int y) { correct implementation here }

Ora se vogliamo calcolare una stringa data una doppia:

double d = whatever;
string r = G(F(d));

G e F possono essere composti in una soluzione corretta al problema più complesso.

Ma i blocchi non hanno questa proprietà a causa dei deadlock. Un metodo M1 corretto che accetta i blocchi nell'ordine L1, L2 e un metodo M2 corretto che accetta i blocchi nell'ordine L2, L1, non possono essere entrambi utilizzati nello stesso programma senza creare un programma errato. I blocchi rendono impossibile dire "ogni singolo metodo è corretto, quindi tutto è corretto".

Quindi, cosa possiamo fare, come designer linguistici?

Primo, non andarci. Più thread di controllo in un programma sono una cattiva idea e condividere la memoria tra thread è una cattiva idea, quindi non metterlo nella lingua o nel runtime in primo luogo.

Questo a quanto pare non è un antipasto.

Rivolgiamo quindi la nostra attenzione alla domanda più fondamentale: perché abbiamo più thread in primo luogo? Ci sono due ragioni principali e spesso si confondono nella stessa cosa, sebbene siano molto diverse. Sono concentrati perché entrambi riguardano la gestione della latenza.

  • Creiamo thread, erroneamente, per gestire la latenza IO. È necessario scrivere un file di grandi dimensioni, accedere a un database remoto, qualunque cosa, creare un thread di lavoro anziché bloccare il thread dell'interfaccia utente.

Cattiva idea. Utilizzare invece asincronia a thread singolo tramite coroutine. C # lo fa magnificamente. Java, non così bene. Ma questo è il modo principale in cui l'attuale gruppo di designer linguistici sta contribuendo a risolvere il problema del threading. L' awaitoperatore in C # (ispirato ai flussi di lavoro asincroni F # e ad altre tecniche precedenti) viene incorporato in un numero sempre maggiore di lingue.

  • Creiamo thread, correttamente, per saturare CPU inattive con un lavoro pesante dal punto di vista computazionale. Fondamentalmente, stiamo usando i thread come processi leggeri.

I progettisti linguistici possono aiutare creando funzionalità linguistiche che funzionano bene con il parallelismo. Pensa a come LINQ viene esteso in modo così naturale a PLINQ, ad esempio. Se sei una persona sensibile e limiti le tue operazioni TPL a operazioni legate alla CPU che sono altamente parallele e non condividono la memoria, puoi ottenere grandi vincite qui.

Cos'altro possiamo fare?

  • Fai in modo che il compilatore rilevi gli errori più complessi e trasformali in avvisi o errori.

C # non ti consente di aspettare in una serratura, perché è una ricetta per i deadlock. C # non ti consente di bloccare un tipo di valore perché è sempre la cosa sbagliata da fare; si blocca la scatola, non il valore. C # ti avvisa se hai un alias volatile, perché l'alias non impone la semantica di acquisizione / rilascio. Esistono molti altri modi in cui il compilatore può rilevare problemi comuni e prevenirli.

  • Progettare funzionalità "di qualità", in cui il modo più naturale per farlo è anche il modo più corretto.

C # e Java hanno commesso un errore di progettazione enorme consentendo di utilizzare qualsiasi oggetto di riferimento come monitor. Ciò incoraggia ogni sorta di cattive pratiche che rendono più difficile rintracciare i deadlock e più difficile prevenirli staticamente. E spreca byte in ogni intestazione di oggetto. I monitor dovrebbero essere derivati ​​da una classe di monitoraggio.

  • Un'enorme quantità di tempo e sforzi di Microsoft Research è andata nel tentativo di aggiungere memoria transazionale del software a un linguaggio simile a C # e non sono mai riusciti a farlo funzionare abbastanza bene da incorporarlo nella lingua principale.

STM è una bellissima idea e ho giocato con le implementazioni di giocattoli in Haskell; consente di comporre in modo molto più elegante soluzioni corrette da parti corrette rispetto alle soluzioni basate su blocchi. Tuttavia, non conosco abbastanza i dettagli per dire perché non si possa far funzionare su larga scala; chiedi a Joe Duffy la prossima volta che lo vedi.

  • Un'altra risposta ha già menzionato l'immutabilità. Se hai l'immutabilità combinata con coroutine efficienti, puoi creare funzionalità come il modello dell'attore direttamente nella tua lingua; pensa Erlang, per esempio.

Sono state fatte molte ricerche sui linguaggi basati sui calcoli di processo e non capisco molto bene quello spazio; prova a leggere alcuni articoli su di te e vedi se hai delle intuizioni.

  • Semplifica la scrittura da parte di terzi di buoni analizzatori

Dopo aver lavorato alla Microsoft su Roslyn, ho lavorato alla Coverity e una delle cose che ho fatto è stata ottenere il front-end dell'analizzatore usando Roslyn. Avendo un'accurata analisi lessicale, sintattica e semantica fornita da Microsoft, potremmo quindi concentrarci sul duro lavoro di scrivere rivelatori che hanno riscontrato problemi comuni di multithreading.

  • Aumenta il livello di astrazione

Un motivo fondamentale per cui abbiamo razze e deadlock e tutta quella roba è perché stiamo scrivendo programmi che dicono cosa fare , e si scopre che siamo tutti schifosi nello scrivere programmi imperativi; il computer fa quello che dici e noi diciamo che fa le cose sbagliate. Molti linguaggi di programmazione moderni riguardano sempre di più la programmazione dichiarativa: dì quali risultati vuoi e lascia che il compilatore capisca il modo efficace, sicuro e corretto per raggiungere quel risultato. Ancora una volta, pensa a LINQ; vogliamo che tu dica from c in customers select c.FirstName, che esprime un intento . Consenti al compilatore di capire come scrivere il codice.

  • Utilizzare i computer per risolvere i problemi del computer.

Gli algoritmi di apprendimento automatico sono molto più efficaci in alcuni compiti rispetto agli algoritmi codificati a mano, anche se ovviamente ci sono molti compromessi tra cui correttezza, tempo impiegato per la formazione, distorsioni introdotte da una cattiva formazione e così via. Ma è probabile che molte attività che codifichiamo attualmente "a mano" saranno presto suscettibili di soluzioni generate dalla macchina. Se gli umani non stanno scrivendo il codice, non stanno scrivendo bug.

Mi dispiace che fosse un po 'sconclusionato lì; questo è un argomento enorme e difficile e non è emerso un chiaro consenso nella comunità PL nei 20 anni in cui ho seguito i progressi in questo spazio problematico.


"Quindi la tua domanda si riduce sostanzialmente a" c'è un modo in cui un linguaggio di programmazione può garantire che il mio programma sia corretto? "E la risposta a questa domanda è, in pratica, no." - in realtà, è del tutto possibile - si chiama verifica formale e, sebbene scomodo, sono abbastanza sicuro che venga eseguito di routine su software critico, quindi non lo definirei impraticabile. Ma tu che sei un designer linguistico probabilmente lo sai ...
Mrpyo,

6
@mrpyo: lo so bene. Ci sono molti problemi Primo: una volta ho partecipato a una conferenza di verifica formale in cui un gruppo di ricerca di MSFT ha presentato un nuovo entusiasmante risultato: sono stati in grado di estendere la loro tecnica per verificare i programmi multithread della lunghezza massima di venti righe e far funzionare il verificatore in meno di una settimana. Questa è stata una presentazione interessante, ma per me inutile; Avevo un programma da 20 milioni di linee da analizzare.
Eric Lippert,

@mrpyo: Secondo, come ho già detto, un grosso problema con i blocchi è che un programma fatto di metodi thread-safe non è necessariamente un programma thread-safe. La verifica formale dei singoli metodi non è necessariamente utile e l'analisi dell'intero programma è difficile per i programmi non banali.
Eric Lippert,

6
@mrpyo: Terzo, il grosso problema con l'analisi formale è che cosa, fondamentalmente, stiamo facendo? Stiamo presentando una specifica di precondizioni e postcondizioni e quindi verificando che il programma soddisfi tale specifica. Grande; in teoria è totalmente fattibile. In quale lingua è scritta la specifica? Se c'è un inequivocabile, verificabile linguaggio di specifica poi facciamo solo scrivere tutti i nostri programmi in quella lingua , e compilare quello . Perché non lo facciamo? Perché risulta davvero difficile scrivere programmi corretti anche nel linguaggio delle specifiche!
Eric Lippert,

2
È possibile analizzare un'applicazione per la correttezza utilizzando pre-condizioni / post-condizioni (ad es. Utilizzando Contratti di codifica). Tuttavia, tale analisi è possibile solo a condizione che le condizioni siano componibili, mentre i blocchi non lo sono. Noterò anche che scrivere un programma in modo da consentire l'analisi richiede un'attenta disciplina. Ad esempio, le applicazioni che non rispettano rigorosamente il principio di sostituzione di Liskov tendono a resistere all'analisi.
Brian,
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.