Le attuali implementazioni "senza blocco" seguono lo stesso schema per la maggior parte del tempo:
- * leggi un po 'di stato e creane una copia **
- * modifica copia **
- eseguire un'operazione interbloccata
- riprova se fallisce
(* opzionale: dipende dalla struttura dati / algoritmo)
L'ultimo pezzo è stranamente simile a uno spinlock. In effetti, è uno spinlock di base . :)
Sono d'accordo con @nobugz su questo: il costo delle operazioni interbloccate utilizzate nel multi-threading senza blocchi è dominato dalle attività di cache e coerenza della memoria che deve svolgere .
Ciò che si guadagna, tuttavia, con una struttura dati "priva di blocchi" è che i "lucchetti" hanno una grana molto fine . Ciò riduce la possibilità che due thread simultanei accedano allo stesso "blocco" (posizione di memoria).
Il trucco la maggior parte delle volte è che non hai blocchi dedicati - invece tratti, ad esempio, tutti gli elementi in un array o tutti i nodi in un elenco collegato come uno "spin-lock". Leggi, modifichi e provi ad aggiornare se non ci sono stati aggiornamenti dall'ultima lettura. Se c'era, riprova.
Questo rende il tuo "blocco" (oh, scusa, non bloccante :) a grana fine, senza introdurre requisiti aggiuntivi di memoria o risorse.
Rendendolo più a grana fine diminuisce la probabilità di attese. Renderlo il più dettagliato possibile senza introdurre requisiti di risorse aggiuntive sembra fantastico, non è vero?
La maggior parte del divertimento, tuttavia, può derivare dall'assicurare un corretto carico / ordine del negozio .
Contrariamente alle proprie intuizioni, le CPU sono libere di riordinare le letture / scritture della memoria - sono molto intelligenti, tra l'altro: sarà difficile osservarlo da un singolo thread. Tuttavia, si verificheranno problemi quando inizi a eseguire il multi-threading su più core. Le tue intuizioni verranno meno: solo perché un'istruzione è all'inizio del tuo codice, non significa che in realtà accadrà prima. Le CPU possono elaborare le istruzioni fuori ordine: e a loro piace farlo soprattutto alle istruzioni con accessi alla memoria, per nascondere la latenza della memoria principale e fare un uso migliore della loro cache.
Ora, è sicuro contro l'intuizione che una sequenza di codice non fluisce "dall'alto verso il basso", invece funziona come se non ci fosse alcuna sequenza - e può essere chiamata "parco giochi del diavolo". Credo che non sia possibile dare una risposta esatta su quali riordini di carico / negozio avranno luogo. Invece, si parla sempre in termini di mays and mights and cans e si prepara al peggio. "Oh, la CPU potrebbe riordinare questa lettura prima di quella scrittura, quindi è meglio mettere una barriera di memoria proprio qui, in questo punto."
Questioni sono complicate dal fatto che anche questi mays e mights possono differire tra architetture di CPU. Esso potrebbe essere il caso, per esempio, che qualcosa che è garantito per non accadere in un'architettura potrebbe accadere su un altro.
Per ottenere un multi-threading "senza blocco", è necessario comprendere i modelli di memoria.
Ottenere il modello di memoria e le garanzie corretti non è comunque banale, come dimostra questa storia, per cui Intel e AMD hanno apportato alcune correzioni alla documentazione MFENCE
provocando qualche scalpore tra gli sviluppatori di JVM . Come si è scoperto, la documentazione su cui gli sviluppatori hanno fatto affidamento sin dall'inizio non era così precisa in primo luogo.
I blocchi in .NET danno luogo a una barriera di memoria implicita, quindi sei sicuro di usarli (la maggior parte delle volte, cioè ... vedi ad esempio questa grandezza di Joe Duffy - Brad Abrams - Vance Morrison su inizializzazione pigra, blocchi, volatili e memoria barriere. :) (Assicurati di seguire i link in quella pagina.)
Come bonus aggiuntivo, verrai introdotto al modello di memoria .NET in una missione secondaria . :)
C'è anche un "oldie but goldie" di Vance Morrison: What Every Dev Must Know About Multithreaded Apps .
... e ovviamente, come ha detto @Eric , Joe Duffy è una lettura definitiva sull'argomento.
Un buon STM può avvicinarsi il più possibile al blocco a grana fine e probabilmente fornirà prestazioni vicine o alla pari con un'implementazione fatta a mano. Uno di questi è STM.NET dai progetti DevLabs di MS.
Se non sei un fanatico solo .NET, Doug Lea ha fatto un ottimo lavoro in JSR-166 .
Cliff Click ha un'interpretazione interessante delle tabelle hash che non si basa sullo striping dei blocchi, come fanno le tabelle hash simultanee Java e .NET, e sembrano scalare bene fino a 750 CPU.
Se non hai paura di avventurarti nel territorio di Linux, il seguente articolo fornisce ulteriori informazioni sugli interni delle attuali architetture di memoria e su come la condivisione della linea di cache può distruggere le prestazioni: Cosa dovrebbe sapere ogni programmatore sulla memoria .
@ Ben ha fatto molti commenti su MPI: Sono sinceramente d'accordo sul fatto che MPI possa brillare in alcune aree. Una soluzione basata su MPI può essere più facile da ragionare, più facile da implementare e meno soggetta a errori di un'implementazione di blocco a metà che cerca di essere intelligente. (È comunque - soggettivamente - vero anche per una soluzione basata su STM.) Scommetto anche che è anni luce più facile scrivere correttamente un'applicazione decente distribuita ad esempio in Erlang, come suggeriscono molti esempi di successo.
MPI, tuttavia, ha i suoi costi e i suoi problemi quando viene eseguito su un unico sistema multi-core . Ad esempio, a Erlang, ci sono problemi da risolvere intorno alla sincronizzazione della pianificazione dei processi e delle code di messaggi .
Inoltre, in fondo, i sistemi MPI di solito implementano una sorta di pianificazione N: M cooperativa per "processi leggeri". Questo ad esempio significa che c'è un inevitabile cambio di contesto tra processi leggeri. È vero che non si tratta di un "cambio di contesto classico" ma principalmente di un'operazione in spazio utente e può essere eseguita velocemente - tuttavia dubito sinceramente che possa essere portata sotto i 20-200 cicli necessari per un'operazione interbloccata . Il cambio di contesto in modalità utente è sicuramente più lentoanche nella libreria Intel McRT. N: M la programmazione con processi leggeri non è nuova. Gli LWP erano presenti in Solaris per molto tempo. Sono stati abbandonati. C'erano fibre nell'NT. Adesso sono perlopiù una reliquia. C'erano "attivazioni" in NetBSD. Sono stati abbandonati. Linux aveva il suo punto di vista sull'argomento del threading N: M. Sembra essere un po 'morto ormai.
Di tanto in tanto, ci sono nuovi contendenti: ad esempio McRT di Intel , o più recentemente User-Mode Scheduling insieme a ConCRT di Microsoft.
Al livello più basso, fanno ciò che fa uno scheduler MPI N: M. Erlang, o qualsiasi sistema MPI, potrebbe trarre grandi vantaggi dai sistemi SMP sfruttando il nuovo UMS .
Immagino che la domanda dell'OP non riguardi i meriti e gli argomenti soggettivi a favore / contro qualsiasi soluzione, ma se dovessi rispondere, immagino che dipenda dal compito: per costruire strutture dati di base di basso livello e ad alte prestazioni che girano su un un singolo sistema con molti core , le tecniche low-lock / "lock-free" o un STM produrranno i migliori risultati in termini di prestazioni e probabilmente batterebbero una soluzione MPI in qualsiasi momento in termini di prestazioni, anche se le rughe di cui sopra sono appianate ad esempio a Erlang.
Per costruire qualcosa di moderatamente più complesso che gira su un singolo sistema, forse sceglierei il classico blocco a grana grossa o, se le prestazioni sono di grande interesse, un STM.
Per costruire un sistema distribuito, un sistema MPI sarebbe probabilmente una scelta naturale.
Notare che ci sono implementazioni MPI anche per .NET (anche se sembrano non essere così attive).