Esistono pratiche deprecate per la programmazione multithread e multiprocessore che non dovrei più utilizzare?


36

All'inizio di FORTRAN e BASIC, essenzialmente tutti i programmi erano scritti con istruzioni GOTO. Il risultato è stato il codice spaghetti e la soluzione è stata la programmazione strutturata.

Allo stesso modo, i puntatori possono avere caratteristiche difficili da controllare nei nostri programmi. Il C ++ è iniziato con molti puntatori, ma si consiglia l'uso di riferimenti. Biblioteche come STL possono ridurre parte della nostra dipendenza. Esistono anche modi di dire per creare puntatori intelligenti con caratteristiche migliori e alcune versioni di C ++ consentono riferimenti e codice gestito.

Le pratiche di programmazione come eredità e polimorfismo usano molti suggerimenti dietro le quinte (proprio come per, mentre la programmazione strutturata genera codice pieno di istruzioni di diramazione). Lingue come Java eliminano i puntatori e usano la garbage collection per gestire i dati allocati dinamicamente invece che dipendere dai programmatori per abbinare tutte le loro nuove ed eliminare le dichiarazioni.

Nella mia lettura, ho visto esempi di programmazione multi-processo e multi-thread che non sembrano usare i semafori. Usano la stessa cosa con nomi diversi o hanno nuovi modi di strutturare la protezione delle risorse dall'uso simultaneo?

Ad esempio, un esempio specifico di un sistema per la programmazione multithread con processori multicore è OpenMP. Rappresenta una regione critica come segue, senza l'uso di semafori, che sembrano non essere inclusi nell'ambiente.

th_id = omp_get_thread_num();
#pragma omp critical
{
  cout << "Hello World from thread " << th_id << '\n';
}

Questo esempio è un estratto da: http://en.wikipedia.org/wiki/OpenMP

In alternativa, una protezione simile dei thread tra loro utilizzando i semafori con funzioni wait () e signal () potrebbe apparire così:

wait(sem);
th_id = get_thread_num();
cout << "Hello World from thread " << th_id << '\n';
signal(sem);

In questo esempio, le cose sono piuttosto semplici e basta una semplice revisione per mostrare che le chiamate wait () e signal () sono abbinate e anche con molta concorrenza, viene fornita la sicurezza del thread. Ma altri algoritmi sono più complicati e utilizzano più semafori (sia binari che di conteggio) distribuiti su più funzioni con condizioni complesse che possono essere chiamate da molti thread. Le conseguenze della creazione di deadlock o della mancata sicurezza delle discussioni possono essere difficili da gestire.

Questi sistemi come OpenMP eliminano i problemi con i semafori?
Spostano il problema altrove?
Come faccio a trasformare il mio semaforo preferito usando l'algoritmo per non usare più i semafori?


Di cosa stai parlando esattamente? Che cosa hai visto?
svick

4
Non significa essere scortesi, ma avresti potuto eliminare i primi tre paragrafi. In realtà non tengono conto della tua domanda e superano le loro conclusioni e genereranno solo molte discussioni.
dbracey,

1
Whoa, grande modifica. Ho provato a rispondere. La domanda continua a vagare attraverso GOTO, puntatori, ereditarietà e polimorfismo, ma nella mia risposta ho messo da parte questi problemi e mi sono concentrato sulla domanda "pratiche deprecate".
Stuart segna il

Risposte:


15

Esistono tecniche e pratiche di programmazione simultanee che non si dovrebbero più usare? Direi di sì .

Una prima tecnica di programmazione concorrente che al giorno d'oggi sembra rara è la programmazione guidata da interruzioni . Ecco come ha funzionato UNIX negli anni '70. Vedere il commento dei Lions su UNIX o Progettazione del sistema operativo UNIX di Bach . In breve, la tecnica consiste nel sospendere temporaneamente gli interrupt durante la manipolazione di una struttura di dati e successivamente ripristinare gli interrupt. La pagina man di BSD spl (9)ha un esempio di questo stile di codifica. Si noti che gli interrupt sono orientati all'hardware e il codice incarna una relazione implicita tra il tipo di interrupt di processo e le strutture dati associate a tale hardware. Ad esempio, il codice che manipola i buffer di I / O del disco deve sospendere gli interrupt dall'hardware del controller del disco mentre si lavora con quei buffer.

Questo stile di programmazione è stato utilizzato dai sistemi operativi su hardware uniprocessore. Era molto più raro per le applicazioni gestire gli interrupt. Alcuni sistemi operativi presentavano interruzioni del software e penso che le persone abbiano provato a costruire sistemi di threading o coroutine su di essi, ma questo non era molto diffuso. (Certamente non nel mondo UNIX.) Sospetto che la programmazione in stile interruzione sia limitata oggi a piccoli sistemi embedded o sistemi in tempo reale.

I semafori rappresentano un progresso rispetto agli interrupt perché sono costrutti software (non correlati all'hardware), forniscono astrazioni rispetto alle strutture hardware e consentono il multithreading e il multiprocessing. Il problema principale è che non sono strutturati. Il programmatore è responsabile del mantenimento della relazione tra ciascun semaforo e le strutture di dati che protegge, a livello globale attraverso l'intero programma. Per questo motivo penso che oggi i semafori nudi siano usati raramente.

Un altro piccolo passo avanti è un monitor , che incapsula i meccanismi di controllo della concorrenza (blocchi e condizioni) con i dati protetti. Questo è stato trasferito nel sistema Mesa (collegamento alternativo) e da lì in Java. (Se leggi questo documento di Mesa, puoi vedere che i blocchi e le condizioni del monitor Java sono copiati quasi alla lettera da Mesa.) I monitor sono utili in quanto un programmatore sufficientemente attento e diligente può scrivere in modo sicuro programmi simultanei usando solo ragionamenti locali sul codice e sui dati all'interno del monitor.

Esistono costrutti di libreria aggiuntivi, come quelli nel java.util.concurrentpacchetto Java , che include una varietà di strutture di dati e costrutti di pool di thread altamente concorrenti. Questi possono essere combinati con tecniche aggiuntive come il confinamento del filo e l'immutabilità effettiva. Vedi Java Concurrency In Practice di Goetz et. al. per ulteriori discussioni. Sfortunatamente, molti programmatori stanno ancora realizzando le proprie strutture di dati con blocchi e condizioni, quando dovrebbero davvero usare qualcosa come ConcurrentHashMap in cui il sollevamento pesante è già stato fatto dagli autori della biblioteca.

Tutto quanto sopra ha alcune caratteristiche significative: hanno molteplici thread di controllo che interagiscono su uno stato mutabile condiviso a livello globale . Il problema è che la programmazione in questo stile è ancora fortemente soggetta a errori. È abbastanza facile per un piccolo errore passare inosservato, causando comportamenti scorretti difficili da riprodurre e diagnosticare. Può darsi che nessun programmatore sia "sufficientemente attento e diligente" per sviluppare grandi sistemi in questo modo. Almeno pochissimi lo sono. Quindi, direi che la programmazione multi-thread con stato condiviso e mutevole dovrebbe essere evitata, se possibile.

Sfortunatamente non è del tutto chiaro se possa essere evitato in tutti i casi. Molta programmazione è ancora fatta in questo modo. Sarebbe bello vederlo soppiantato da qualcos'altro. Le risposte di Jarrod Roberson e davidk01 indicano tecniche come dati immutabili, programmazione funzionale, STM e trasmissione di messaggi. C'è molto da consigliarli e tutti vengono sviluppati attivamente. Ma non credo che abbiano ancora completamente sostituito il buono stato mutevole vecchio stile.

EDIT: ecco la mia risposta alle domande specifiche alla fine.

Non so molto di OpenMP. La mia impressione è che può essere molto efficace per problemi altamente paralleli come le simulazioni numeriche. Ma non sembra generico. I costrutti dei semafori sembrano piuttosto di basso livello e richiedono al programmatore di mantenere la relazione tra semafori e strutture di dati condivisi, con tutti i problemi che ho descritto sopra.

Se hai un algoritmo parallelo che utilizza i semafori, non conosco alcuna tecnica generale per trasformarlo. Potresti essere in grado di trasformarlo in oggetti e quindi costruire alcune astrazioni attorno ad esso. Ma se vuoi usare qualcosa come il passaggio di messaggi, penso che tu abbia davvero bisogno di riconcettualizzare l'intero problema.


Grazie, questa è un'ottima informazione. Esaminerò i riferimenti e mi immergerò più in profondità nei concetti che mi dici che sono nuovi per me.
Sviluppatore:

+1 per java.util.concurrent e concordato sul commento - è stato nel JDK dall'1.5 e raramente lo vedo mai usato.
MebAlone,

1
Vorrei che tu abbia sottolineato quanto sia importante non rotolare le tue strutture quando ne esistono già. Così tanti, così tanti bug ...
corsiKa

Non credo sia corretto dire "I semafori sono un anticipo rispetto agli interrupt perché sono costrutti software (non correlati all'hardware) ". I semafori dipendono dalla CPU per implementare l' istruzione Compare-and-Swap o dalle sue varianti multi-core .
Josh Pearce,

@JoshPearce Naturalmente i semafori sono implementati usando costrutti hardware, ma sono un'astrazione indipendente da qualsiasi particolare costrutto hardware, come CAS, test-and-set, cmpxchng, ecc.
Stuart Marks

28

Rispondi alla domanda

Il consenso generale è che lo stato mutabile condiviso è Bad ™, e lo stato immutabile è Good ™, che si è dimostrato accurato e vero ancora e ancora dai linguaggi funzionali e dalle lingue imperative.

Il problema è che le lingue imperative tradizionali non sono progettate per gestire questo modo di lavorare, le cose non cambieranno per quelle lingue durante la notte. Questo è dove il confronto GOTOè imperfetto. Lo stato immutabile e il passaggio di messaggi è un'ottima soluzione ma non è nemmeno una panacea.

Premessa imperfetta

Questa domanda si basa sul confronto con una premessa errata; quello GOTOera il vero problema ed era universalmente deprecato in qualche modo dal Consiglio Universale Intergalatico dei progettisti di linguaggi e dei Software Engineering Unions ©! Senza un GOTOmeccanismo ASM non funzionerebbe affatto. Lo stesso con la premessa che i puntatori grezzi sono il problema con C o C ++ e alcuni come i puntatori intelligenti sono una panacea, non lo sono.

GOTOnon era il problema, i programmatori erano il problema. Lo stesso vale per lo stato mutabile condiviso . Di per sé non è il problema , sono i programmatori che lo usano che è il problema. Se ci fosse un modo per generare codice che utilizzava uno stato mutabile condiviso in un modo che non ha mai avuto condizioni di competizione o bug, allora non sarebbe un problema. Proprio come se non si scrivesse mai il codice spaghetti GOTOo con costrutti equivalenti, non è nemmeno un problema.

L'istruzione è la panacea

I programmatori idioti sono ciò che erano deprecated, ogni linguaggio popolare ha ancora il GOTOcostrutto direttamente o indirettamente ed è un best practicequando usato correttamente in ogni linguaggio che ha questo tipo di costrutti.

ESEMPIO: Java ha etichette ed try/catch/finallyentrambe funzionano direttamente come GOTOistruzioni.

La maggior parte dei programmatori Java con cui parlo non sa nemmeno cosa immutablesignifichi realmente fuori di loro ripetere the String class is immutablecon uno sguardo da zombi nei loro occhi. Sicuramente non sanno come usare finalcorrettamente la parola chiave per creare una immutableclasse. Quindi sono abbastanza sicuro che non abbiano idea del perché il passaggio di messaggi utilizzando messaggi immutabili sia così grande e perché lo stato mutabile condiviso non sia così grande.


3
+1 Ottima risposta, scritta chiaramente e indicando il modello sottostante di stato mutevole. IUBLDSEU dovrebbe diventare un meme :)
Dibbeke il

2
GOTO è una parola in codice per "per favore, no davvero per favore inizia una guerra di fiamma qui, doppio cane ti sfido". Questa domanda spegne le fiamme ma in realtà non dà una buona risposta. Le menzioni d'onore della programmazione funzionale e dell'immutabilità sono fantastiche, ma non c'è carne in quelle affermazioni.
Evan Plaice,

1
Questa sembra essere una risposta contraddittoria. Innanzitutto, dici "A è cattivo, B è buono", quindi dici "Gli idioti sono stati deprecati". La stessa cosa non si applica al primo paragrafo? Non posso semplicemente prendere l'ultima parte della tua risposta e dire "Lo stato mutevole condiviso è una buona pratica se usato correttamente in ogni lingua". Inoltre, "prova" è una parola molto forte. Non dovresti usarlo se non hai prove davvero forti.
luiscubal,

2
Non era mia intenzione iniziare una guerra di fiamme. Fino a quando Jarrod non ha reagito al mio commento, aveva pensato che GOTO non fosse controverso e avrebbe funzionato bene in analogia. Quando ho scritto la domanda, non mi è venuta in mente, ma Dijkstra era al punto zero su GOTO e semafori. Edsger Dijkstra mi sembra un gigante e mi è stata attribuita l'invenzione dei semafori (1965) e dei primi (1968) lavori accademici sui GOTO. Il metodo di difesa di Dijkstra era spesso crudele e conflittuale. Controversie / scontri hanno funzionato per lui, ma voglio solo idee su possibili alternative ai semafori.
Sviluppatore:

1
Molti programmi dovrebbero modellare cose che, nel mondo reale, sono mutabili. Se alle 5:37, l'oggetto # 451 detiene lo stato di qualcosa nel mondo reale in quel momento (5:37), e lo stato della cosa del mondo reale successivamente cambia, è possibile l'identità dell'oggetto che rappresenta lo stato della cosa del mondo reale è immutabile (cioè la cosa sarà sempre rappresentata dall'oggetto # 451), o affinché l'oggetto # 451 sia immutabile, ma non entrambi. In molti casi, avere l'identità immutabile sarà più utile che avere l'oggetto # 451 essere immutabile.
supercat

27

L'ultima rabbia nei circoli accademici sembra essere la Software Transactional Memory (STM) e promette di togliere tutti i dettagli pelosi della programmazione multi-thread dalle mani dei programmatori usando una tecnologia di compilazione sufficientemente intelligente. Dietro le quinte ci sono ancora lucchetti e semafori ma tu come programmatore non devi preoccuparti. I vantaggi di questo approccio non sono ancora chiari e non ci sono contendenti ovvi.

Erlang utilizza il passaggio di messaggi e gli agenti per la concorrenza e questo è un modello più semplice con cui lavorare rispetto a STM. Con il passaggio dei messaggi non devi assolutamente preoccuparti di blocchi e semafori perché ogni agente opera nel suo mini universo, quindi non ci sono condizioni di competizione relative ai dati. Hai ancora alcuni strani casi limite ma non sono affatto complicati come i livelock e gli deadlock. Le lingue JVM possono fare uso di Akka e ottenere tutti i vantaggi del passaggio di messaggi e degli attori, ma a differenza di Erlang la JVM non ha il supporto integrato per gli attori, quindi alla fine Akka fa ancora uso di thread e blocchi ma tu come il programmatore non deve preoccuparsene.

L'altro modello di cui sono a conoscenza che non utilizza blocchi e thread è utilizzando i futures, che è in realtà solo un'altra forma di programmazione asincrona.

Non sono sicuro di quanta tecnologia sia disponibile in C ++, ma è probabile che se vedi qualcosa che non utilizza esplicitamente thread e blocchi, sarà una delle tecniche di cui sopra per la gestione della concorrenza.


+1 per il nuovo termine "dettagli pelosi". LOL amico. Non riesco proprio a smettere di ridere di questo nuovo mandato. Immagino che da ora in poi userò il "codice peloso".
Saeed Neamati,

1
@Saeed: ho già sentito quell'espressione, non è così raro. Sono d'accordo però è divertente :-)
Cameron il

1
Buona risposta. La CLI .NET presumibilmente ha anche il supporto per la segnalazione (al contrario del blocco) ma devo ancora imbattermi in un esempio in cui ha completamente sostituito il blocco. Non sono sicuro se l'asincrono conta. Se stai parlando di piattaforme come Javascript / NodeJs, in realtà sono single-thread e sono migliori solo con elevati carichi di IO perché sono molto meno sensibili ai limiti massimi di risorse (cioè su una tonnellata di contesti usa e getta). Sui carichi intensivi della CPU c'è poco / nessun vantaggio nell'uso della programmazione asincrona.
Evan Plaice,

1
Risposta interessante, non avevo mai incontrato il futuro prima. Si noti inoltre che è ancora possibile avere deadlock e livelock nei sistemi di passaggio dei messaggi come Erlang . CSP ti consente di ragionare formalmente su deadlock e livelock ma non lo impedisce da solo.
Mark Booth,

1
Vorrei aggiungere Lock gratuito e attendere strutture dati gratuite a tale elenco.
stonemetal

3

Penso che si tratti principalmente di livelli di astrazione. Abbastanza spesso nella programmazione, è utile sottrarre alcuni dettagli in un modo più sicuro o più leggibile o qualcosa del genere.

Questo vale per le strutture di controllo: ifs, forse pari try- i catchblocchi sono solo astrazioni su gotos. Queste astrazioni sono quasi sempre utili, perché rendono il tuo codice più leggibile. Ma ci sono casi in cui dovrai ancora utilizzarli goto(ad es. Se stai scrivendo un assemblaggio a mano).

Questo vale anche per la gestione della memoria: i puntatori intelligenti C ++ e GC sono astrazioni sui puntatori non elaborati e la de / allocazione manuale della memoria. E a volte, queste astrazioni non sono appropriate, ad esempio quando hai davvero bisogno delle massime prestazioni.

E lo stesso vale per il multi-threading: cose come futures e attori sono solo astrazioni su thread, semafori, mutex e istruzioni CAS. Tali astrazioni possono aiutarti a rendere il tuo codice molto più leggibile e ti aiutano anche a evitare errori. Ma a volte, semplicemente non sono appropriati.

Dovresti sapere quali strumenti hai a disposizione e quali sono i loro vantaggi e svantaggi. Quindi puoi scegliere l'astrazione corretta per il tuo compito (se presente). Livelli più alti di astrazione non deprezzano i livelli più bassi, ci saranno sempre alcuni casi in cui l'astrazione non è appropriata e la scelta migliore è quella di usare il "vecchio modo".


Grazie, stai cogliendo l'analogia, e non ho un'idea preconcetta o nemmeno un'ascia da capire se la risposta ai semafori WRT è che sono o non sono deprecati. La domanda più grande per me è che ci sono modi migliori e nei sistemi che sembrano non avere semafori mancanti di qualcosa di importante e non sarebbero in grado di fare l'intera gamma di algoritmi multithread.
Sviluppatore:

2

Sì, ma non è probabile che ti imbatterai in alcuni di essi.

Ai vecchi tempi, era comune usare metodi di blocco (sincronizzazione della barriera) perché scrivere buoni mutex era difficile da fare nel modo giusto. Puoi ancora vedere tracce di ciò in cose recenti. L'uso delle moderne librerie di concorrenza ti offre un set di strumenti molto più ricco e testato a fondo per la parallelizzazione e il coordinamento tra processi.

Allo stesso modo, una pratica più antica era quella di scrivere un codice tortuoso in modo da poter capire come parallelizzarlo manualmente. Questa forma di ottimizzazione (potenzialmente dannosa, se sbagli) è stata in gran parte fuori dalla finestra con l'avvento dei compilatori che lo fanno per te, svolgendo cicli se necessario, seguendo in modo predittivo rami, ecc. Questa non è una nuova tecnologia, tuttavia , essendo almeno 15 anni sul mercato. Sfruttare cose come i pool di thread elude anche un codice davvero complicato di una volta.

Quindi forse la pratica deprecata sta scrivendo tu stesso il codice di concorrenza, invece di usare librerie moderne e ben collaudate.


Grazie. Sembra che ci sia un grande potenziale per l'uso della programmazione concorrente, ma potrebbe essere un vaso di Pandora se non usato in modo disciplinato.
Sviluppatore:

2

Grand Central Dispatch di Apple è un'elegante astrazione che ha cambiato il mio pensiero sulla concorrenza. La sua attenzione alle code rende l'implementazione della logica asincrona un ordine di grandezza più semplice, nella mia umile esperienza.

Quando programmavo in ambienti in cui era disponibile, aveva sostituito la maggior parte dei miei usi di thread, blocchi e comunicazioni inter-thread.


1

Una delle principali modifiche alla programmazione parallela è che le CPU sono tremendamente più veloci di prima, ma per ottenere tali prestazioni, è necessaria una cache ben riempita. Se provi a eseguire diversi thread contemporaneamente scambiandoli continuamente tra loro, quasi sempre invalidi la cache per ogni thread (cioè ogni thread richiede dati diversi per funzionare) e finisci per uccidere le prestazioni molto più di te abituato a CPU più lente.

Questo è uno dei motivi per cui i framework asincroni o basati su attività (ad es. Grand Central Dispatch o TBB di Intel) sono più popolari, eseguono attività di codice 1 alla volta, finendo prima di passare al successivo - tuttavia, è necessario codificare ciascuno ogni attività richiede poco tempo a meno che tu non voglia rovinare il design (cioè le tue attività parallele sono davvero in coda). Le attività ad uso intensivo di CPU vengono passate a un core CPU alternativo anziché elaborate sul singolo thread che elabora tutte le attività. È anche più facile da gestire se non è in corso un'elaborazione multi-thread.


Fantastico, grazie per i riferimenti alla tecnologia Apple e Intel. La tua risposta sta mettendo in evidenza le sfide della gestione del thread con l'affinità principale? Alcuni problemi di prestazioni della cache sono ridotti perché i processori multicore possono ripetere le cache L1 per core? Ad esempio: software.intel.com/en-us/articles/… La cache ad alta velocità per quattro core con più accessi alla cache può essere più di 4x più veloce di un core con più errori di cache sugli stessi dati. La moltiplicazione delle matrici può. La programmazione casuale di 32 thread su 4 core non può. Usiamo l'affinità e otteniamo 32 core.
Sviluppatore:

non proprio se è lo stesso problema: l'affinità core si riferisce solo al problema in cui un'attività viene rimbalzata da core a core. È lo stesso problema se un'attività viene interrotta, sostituita con una nuova, quindi l'attività originale continua sullo stesso core. Intel sta dicendo lì: hit della cache = veloce, errori della cache = lento, indipendentemente dal numero di core. Penso che stiano cercando di convincerti ad acquistare i loro chip invece di AMD :)
gbjbaanb
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.