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' await
operatore 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.