Posts tagged ‘concorrenza’

Cos’e’ la concorrenza in informatica?

In informatica la concorrenza è una caratteristica dei sistemi nei quali può verificarsi che un insieme di processi computazionali sia in esecuzione nello stesso istante. Tale sistema viene appunto chiamato sistema a concorrenza o sistema concorrente. L'esecuzione parallela può condurre a interazione tra processi quando viene coinvolta una risorsa condivisa.

Un'importante classe di sistemi informatici nei quali gli aspetti di concorrenza sono fondamentali è quella dei sistemi operativi.

Il problema dei filosofi affamati, un classico problema inerente concorrenza e condivisione di risorse

Il concetto di concorrenza è contrapposto a quello di sequenzialità. In un sistema sequenziale i processi vengono eseguiti uno per volta e non si verifica alcuna forma di interazione tra essi durante l'esecuzione.

Introduzione

Si può parlare di concorrenza nel caso di:

  • parallelismo reale di esecuzione (nel caso di sistemi multiprocessore dove si possono eseguire parallelamente un numero di processi pari al numero di processori)
  • parallelismo virtuale di esecuzione (come nel caso del pipelining).

Dato che in un sistema concorrente la varietà di interazioni che si possono verificare tra processi in esecuzione è notevole, sono state elaborate delle teorie in merito alla gestione delle problematiche connesse alla concorrenza. Sulla base di queste teorie sono state sviluppate sia delle tecniche di programmazione sia dei linguaggi che integrano nativamente primitive per la corretta gestione della concorrenza.


Problematiche

La concorrenza può portare ad una serie di problematiche legate all'utilizzo di una stessa risorsa condivisa da parte di più processi. Un determinato susseguirsi di eventi può causare condizioni critiche. La programmazione concorrente sfrutta alcuni principi per fronteggiare e risolvere questo tipo di problematiche.

  • Corse critiche (Race Conditions)
    In alcuni sistemi può accadere che i processi in esecuzione condividano una risorsa comune di qualsiasi natura (sia essa un'area di memoria condivisa o una periferica). In particolare se si verifica che il risultato finale dell'esecuzione di più processi dipende dall'ordine in cui essi vengono eseguiti, questa è una condizione di corsa critica (race condition). Il risultato dell'esecuzione nel caso di corse critiche è assolutamente impredicibile.

Il problema delle "corse critiche" può essere evitato impedendo che più di un processo per volta acceda a risorse condivise. Con la mutua esclusione si evita che più processi che contendono una risorsa riescano ad accedervi contemporaneamente.

  • Stallo (Deadlock)
    Quando ad un processo viene garantito l'accesso esclusivo (ad esempio tramite una mutua esclusione) ad una risorsa, possono crearsi situazioni di stallo. Formalmente un insieme di processi è in stallo (deadlock) quando ogni processo dell'insieme attende un evento che può avvenire soltanto tramite un altro processo dell'insieme. Essendo tutti i processi in attesa, nessuno potrà mai creare l'evento di sblocco protraendo l'attesa all'infinito. Le tecniche per individuare situazioni di stallo prevedono l'analisi di grafi delle risorse allocate oppure mediante la creazione di cosiddette "matrici di allocazione". La risoluzione degli stalli può essere affrontata in vari modi. Concettualmente si possono suddividere in:

    • Risoluzione mediante prerilascio: viene scelto un processo che detiene una risorsa dall'insieme dei processi in stallo e viene tolto l'accesso esclusivo (prerilascio o preemption) ad una risorsa condivisa (causa di stallo). L'operazione è talvolta difficile, talvolta impossibile e dipende dal tipo di risorsa che il processo stava bloccando.
    • Risoluzione mediante punto di controllo: vengono creati dei registri (checkpoint) che descrivono lo stato di utilizzo delle risorse condivise. Quando viene rilevato uno stallo si effettua un "ritorno" (rollback to checkpoint) alle condizioni precedenti. Anche questa tecnica è di difficile o addirittura impossibile realizzazione poiché il ritorno potrebbe causare perdita o inconsistenza di dati.
    • Risoluzione mediante eliminazione: viene scelto un processo e viene terminato. Questa tecnica è anch'essa molto complessa e richiede di fare stime e assunzioni sul tipo di processo da eliminare. Inoltre non è garantita l'uscita dalla condizione di stallo per cui potrebbe essere necessario terminare altri processi, situazione che complica ulteriormente la problematica.

È anche possibile effettuare delle stime sulle risorse che verranno impegnate da un processo. Grazie a tali stime, esistono sistemi in cui invece di risolvere le situazioni di stallo risulta più semplice evitarle a priori.

Tutte le tecniche prevedono la costruzione di matrici che tengono traccia dell'utilizzo delle risorse (matrici di traiettoria di risorse) o si basano su algoritmi noti come l'algoritmo del banchiere.

  • Starvation
    Letteralmente inedia, è un problema in stretta relazione con lo stallo. Quando le politiche di assegnazione delle risorse condivise favoriscono un processo rispetto ad un altro, il processo a cui vengono assegnate in minor misura soffre di starvation. Esso è infatti bloccato e non può proseguire sebbene non si trovi in una condizione di stallo. Nei sistemi in cui si accede a risorse condivise è sempre necessario stabilire una politica per le priorità e l'ordine con cui esse vengono ripartite. Sebbene queste politiche possano risultare quanto più eque, esse possono portare a condizioni di starvation.


Comunicazione InterProcesso

Nell'ambito della programmazione concorrente la comunicazione interprocesso (IPC o InterProcess Communication) è di fondamentale importanza per gestire correttamente possibili situazioni di accesso a risorse condivise. I sistemi concorrenti mettono a disposizione delle primitive di comunicazione che permettono di alternarsi (escludendosi a vicenda) nell'accesso ad una risorsa condivisa oltre a primitive di sincronizzazione che permettono di intervenire sulla sequenza secondo la quale avverranno determinati eventi. Ogni sistema concorrente implementa e mette a disposizione le proprie primitive, ma nonostante ciò i sistemi si rifanno ad una base teorica comune. Per questo è possibile creare una panoramica d'insieme.

Le più comuni vie di comunicazioni interprocessuali sono:

  • Mutex: variabili binarie che permettono di gestire l'accesso ad una risorsa condivisa mediante accesso esclusivo di uno dei contendenti. Le operazioni di blocco (in termini semplicistici richiesta di accesso alla risorsa condivisa) e sblocco (rilascio della risorsa) sono atomiche.
  • Semafori n-ari: variabili n-arie che possono essere incrementate e decrementate. Il processo che decrementa il semaforo si bloccherà appena raggiunto lo zero della variabile. Un processo che incrementa il semaforo invece risveglia tutti i processi che si erano bloccati in fase di decremento. Questa è una delle primitive base che consentono la sincronizzazione. Le operazioni di incremento, decremento e controllo del semaforo sono atomiche.
  • Variabili di tipo condizione: conosciute anche come Condition Variables, sono variabili con associate due primitive: wait (aspetta) e signal (segnala, risveglia). Una procedura che richiama la primitiva wait si pone in attesa indefinita, una procedura che richiama la primitiva signal ne risveglia una in attesa su wait. Anche in questo caso le operazioni sono atomiche.
  • Scambio di messaggi: due procedure, send (invia) e receive (ricevi), permettono a due processi di scambiarsi messaggi. Lo scambio di messaggi è solitamente usato in sistemi paralleli.
  • Barriere: talvolta le applicazioni sono divise in fasi con la regola che nessun processo può proseguire se prima tutti i suoi simili non sono pronti a farlo. Le barriere implementano questo concetto: un processo che ha terminato la sua fase chiama una primitiva barrier e si blocca. Quando tutti i processi coinvolti hanno terminato il loro stadio di esecuzione invocando anch'essi la primitiva barrier, il sistema li sblocca tutti permettendo di passare ad uno stadio successivo.

Il concetto di atomicità nei sistemi concorrenti ha due fondamentali caratteristiche:

  • La sua esecuzione non può essere prelazionata: il blocco e lo sblocco di un mutex non possono essere interrotti durante la loro esecuzione.
  • La sua esecuzione è unica: non può avvenire che contemporaneamente due processi riescano a bloccare lo stesso mutex. Nei sistemi uniprocessore questa affermazione è una conseguenza della precedente poiché il concetto di "contemporaneità" è solo virtuale.

È evidente che tutte le primitive di comunicazione e sincronizzazione intraprocesso debbano rispettare questa condizione di atomicità.


Problemi classici

Esiste una serie di problemi classici nella programmazione concorrente. Essi vengono utilizzati per dimostrare l'efficienza di determinate teorie od algoritmi e forniscono una base comune per potere effettuare dei paragoni. Tra quelli più famosi si elencano:

  • Produttore e Consumatore: anche conosciuto come problema del buffer a capienza limitata (bounded buffer problem). Un insieme di processi detti produttori producono dati per inserirli in un buffer di dimensioni finite mentre un altro insieme, i consumatori, estrae dati da questo buffer rappresentante la risorsa condivisa. Una programmazione concorrente ideale prevede un accesso esclusivo al buffer e arbitra correttamente il comportamento dei produttori quando il buffer è pieno e dei consumatori quando il buffer è vuoto.
  • Lettori e scrittori: modella l'accesso a basi di dati. Su una base di dati agiscono scrittori, che ne modificano il contenuto, e lettori che ne recuperano il contenuto. Una corretta programmazione concorrente prevede l'accesso di un solo scrittore in fase di modifica (e di nessun lettore onde evitare inconsistenza) mentre l'accesso di tanti lettori è possibile a patto che non ci siano scrittori attivi contemporaneamente.
  • Filosofi a cena: formulato da Edsger Dijkstra come dining philosophers problem. Alcuni filosofi (5 nel testo originale) sono seduti a tavola di fronte al loro piatto ed a due forchette (condivise con i loro vicini). I filosofi alternano momenti durante i quali meditare e momenti durante i quali mangiare. Per mangiare devono prendere le due forchette accanto al loro piatto e mangiare mentre durante la meditazione devono tenere le forchette sul tavolo. Risulta evidente che il numero di forchette impedisce a tutti i filosofi di mangiare contemporaneamente quindi una corretta programmazione concorrente deve essere in grado di far mangiare alternativamente tutti i filosofi evitando che qualcuno in particolare soffra la fame ed evitando che si verifichino stalli in fase di "acquisizione delle forchette".
  • Barbiere che dorme: un barbiere possiede un negozio con una sola sedia da lavoro e un certo numero limitato di posti per attendere. Se non ci sono clienti il barbiere dorme. All'arrivo del primo cliente il barbiere si sveglia ed inizia a servirlo. Se dovessero sopraggiungere clienti durante il periodo di attività del barbiere, essi si mettono in attesa sui posti disponibili. Al termine dei posti di attesa, un ulteriore cliente viene scartato. Questa problematica è molto vicina al sistema di funzionamento degli helpdesk informatizzati dove l'operatore serve, uno per volta, tutti i clienti in coda oppure attende, senza effettuare alcuna operazione in particolare, l'arrivo di nuove chiamate. Una corretta programmazione concorrente deve far "dormire" il barbiere in assenza di clienti, attivare il barbiere sul primo cliente al suo arrivo e mettere in coda tutti i successivi clienti tenendoli inattivi.


    Fonte: http://it.wikipedia.org/wiki/Programmazione_concorrente


Cos’è un design pattern?

Nell'ingegneria del software, un design pattern (schema di progettazione) può essere definito "una soluzione progettuale generale a un problema ricorrente". Esso non è una libreria o un componente di software riusabile, quanto piuttosto una descrizione o un modello da applicare per risolvere un problema che può presentarsi in diverse situazioni durante la progettazione e lo sviluppo del software.

I design pattern orientati agli oggetti tipicamente mostrano relazioni ed interazioni tra classi o oggetti, senza specificare le classi applicative finali coinvolte. Tali pattern risiedono quindi nel dominio dei moduli e delle interconnessioni. Ad un livello più alto sono invece i Pattern architetturali che hanno un ambito ben più ampio, descrivendo un pattern complessivo adottato dall'intero sistema.

La differenza tra un algoritmo e un design pattern è che il primo risolve problemi computazionali, mentre il secondo è legato agli aspetti progettuali del software.

Storia

Il termine fu inizialmente introdotto in architettura in un celebre saggio di Christopher Alexander; in seguito, proprio l'opera di Alexander ispirò la nascita di un settore dell'ingegneria del software dedicato all'applicazione del concetto di design pattern alle architetture software, soprattutto object-oriented. Oggi, l'espressione design pattern viene usata principalmente con riferimento a questo particolare contesto.

Il tema dei pattern viene oggi considerato una delle linee principali di sviluppo dell'ingegneria del software object-oriented. Esso trova applicazioni in tutta una serie di contesti di grande interesse per l'industria del software, dallo sviluppo di software basato su componenti, ai sistemi aperti, ai framework e così via. La maggior parte dei linguaggi di programmazionemoderni, e di tecnologie correlate, sono stati progettati (o modificati) tenendo conto anche dell'obiettivo di essere coerenti con questo approccio emergente allo sviluppo del software.

La nascita del "movimento" dei pattern in informatica si deve al celebre libro Design Patterns: Elementi per il riuso di software ad oggetti di Erich GammaRichard HelmRalph Johnson eJohn Vlissides (1995). Grazie al successo di quest'opera, i suoi quattro autori divennero nomi talmente citati che la comunità scientifica iniziò, per brevità, a identificarli collettivamente con un nomignolo: la "banda dei quattro" (Gang of Four o Gof).

Struttura di un pattern

Un design pattern è costituito da:

  • il nome, costituito da una o due parole che siano il più possibile rappresentative del pattern stesso;
  • il problema, ovvero la descrizione della situazione alla quale si può applicare il pattern. Può comprendere la descrizione di classi o di problemi di progettazione specifici, come anche una lista di condizioni perché sia necessario l'utilizzo del pattern;
  • la soluzione, che descrive gli elementi costitutivi del progetto con le relazioni e relative implicazioni, senza però addentrarsi in una specifica implementazione. Il concetto è di presentare un problema astratto e la relativa configurazione di elementi adatta a risolverlo;
  • le conseguenze, i risultati e i vincoli che derivano dall'applicazione del pattern. Sono fondamentali in quanto possono essere l'ago della bilancia nella scelta dei pattern: le conseguenze comprendono considerazioni di tempo e di spazio, possono descrivere implicazioni del pattern con alcuni linguaggi di programmazione e l'impatto con il resto del progetto.

L'uso di pattern nella descrizione di altri pattern dà origine ai cosiddetti linguaggi di pattern.

Classificazione dei design pattern

I design pattern possono essere classificati con diversi criteri, i più comuni dei quali sono quelli che evidenziano il tipo di problema che si cerca di risolvere. Il tipo di problema può essere legato ad uno specifico dominio progettuale (telecomunicazioni, reti, software…) oppure, più comunemente, al problema progettuale in senso più ampio (nell'ingegneria del software, ad esempio, si può parlare di creazione, comportamento, navigazione di oggetti o strutture dati).

Nel loro libro la "banda dei quattro" identificò 23 tipi di design pattern, suddivisi in 3 categorie: strutturali, creazionali e comportamentali.

Pattern creazionali

I pattern creazionali nascondono i costruttori delle classi e mettono dei metodi al loro posto creando un'interfaccia. In questo modo si possono utilizzare oggetti senza sapere come sono implementati.

  • L'Abstract factory (letteralmente, "fabbrica astratta") fornisce un'interfaccia per creare famiglie di oggetti connessi o dipendenti tra loro, in modo che non ci sia necessità da parte degli utilizzatori di specificare i nomi delle classi concrete all'interno del proprio codice.
  • Il Builder ("costruttore") separa la costruzione di un oggetto complesso dalla sua rappresentazione, in modo che il processo di costruzione stesso possa creare diverse rappresentazioni.
  • Il Factory method ("metodo fabbrica") fornisce un'interfaccia per creare un oggetto, ma lascia che le sottoclassi decidano quale oggetto istanziare.
  • La Lazy initialization ("inizializzazione pigra") è la tattica di instanziare un oggetto solo nel momento in cui deve essere usato per la prima volta. È utilizzato spesso insieme al patternfactory method.
  • Il Prototype pattern ("prototipo") permette di creare nuovi oggetti clonando un oggetto iniziale, o prototipo.
  • Il Singleton ("singoletto") ha lo scopo di assicurare che di una classe possa essere creata una sola istanza.

Pattern strutturali

I pattern strutturali consentono di riutilizzare degli oggetti esistenti fornendo agli utilizzatori un'interfaccia più adatta alle loro esigenze.

  • L'Adapter ("adattatore") converte l'interfaccia di una classe in una interfaccia diversa
  • Bridge ("ponte") permette di separare l'astrazione di una classe dalla sua implementazione, per permettere loro di variare indipendentemente.
  • Il Composite ("composto"), utilizzato per dare la possibilità all'utilizzatore di manipolare gli oggetti in modo uniforme, organizza gli oggetti in una struttura ad albero.
  • Il Container ("contenitore") offre una soluzione alla rottura dell'incapsulamento per via dell'uso dell'ereditarietà.
  • Il Decorator ("decoratore") consente di aggiungere metodi a classi esistenti durante il run-time (cioè durante lo svolgimento del programma), permettendo una maggior flessibilità nell'aggiungere delle funzionalità agli oggetti.
  • Extensibility ("estendibilità")
  • Il Façade ("facciata") permette, attraverso un'interfaccia più semplice, l'accesso a sottosistemi che espongono interfacce complesse e diverse tra loro.
  • Flyweight ("peso piuma"), che permette di separare la parte variabile di una classe dalla parte che può essere riutilizzata.
  • Proxy fornisce una rappresentazione di un oggetto di accesso difficile o che richiede un tempo importante per l’accesso o creazione. Il Proxy consente di posticipare l’accesso o creazione al momento in cui sia davvero richiesto.
  • Pipes and filters ("condotti e filtri")
  • Private class data ("dati di classe privati")

Pattern comportamentali

I pattern comportamentali forniscono soluzione alle più comuni tipologie di interazione tra gli oggetti.

  • Chain of Responsibility ("catena di responsabilità") diminuisce l'accoppiamento fra l'oggetto che effettua una richiesta e quello che la soddisfa, dando a più oggetti la possibilità di soddisfarla
  • Il Command ("comando") permette di isolare la porzione di codice che effettua un'azione dal codice che ne richiede l'esecuzione.
  • Event Listener ("ascoltatore di eventi")
  • Hierarchical Visitor ("visitatore di gerarchia")
  • Interpreter ("interprete") dato un linguaggio, definisce una rappresentazione della sua grammatica insieme ad un interprete che utilizza questa rappresentazione per l'interpretazione delle espressioni in quel determinato linguaggio.
  • L'Iterator ("iteratore") risolve diversi problemi connessi all'accesso e alla navigazione attraverso gli elementi di una struttura dati, senza esporre i dettagli dell'implementazione e della struttura interna del contenitore.
  • Il Mediator ("mediatore") si interpone nelle comunicazioni tra oggetti, allo scopo di aggiornare lo stato del sistema quando uno qualunque di essi comunica un cambiamento del proprio stato.
  • Il design pattern Memento ("promemoria") è l'operazione di estrarre lo stato interno di un oggetto, senza violarne l'incapsulazione, e memorizzarlo per poterlo ripristinare in un momento successivo.
  • L'Observer ("osservatore") definisce una dipendenza uno a molti fra oggetti diversi, in maniera tale che se un oggetto cambia il suo stato, tutti gli oggetti dipendenti vengono notificati del cambiamento avvenuto e possono aggiornarsi.
  • Single-serving Visitor
  • State ("stato") permette ad un oggetto di cambiare il suo comportamento al cambiare di un suo stato interno.
  • Lo Strategy ("strategia") è utile in quelle situazioni dove è necessario modificare dinamicamente gli algoritmi utilizzati da un'applicazione.
  • Il Template method ("metodo schema") permette di definire la struttura di un algoritmo lasciando alle sottoclassi il compito di implementarne alcuni passi come preferiscono.
  • Il Visitor ("visitatore") permette di separare un algoritmo dalla struttura di oggetti composti a cui è applicato, in modo da poter aggiungere nuovi comportamenti senza dover modificare la struttura stessa.


Altri tipi di pattern

Alcuni pattern definiti nella letteratura non operano a livello di design del sistema, non possono quindi essere definiti propriamente design pattern. Alcuni esempi sono:

Pattern architetturali

I pattern architetturali operano ad un livello diverso (e più ampio) rispetto ai design pattern, ed esprimono schemi di base per impostare l'organizzazione strutturale di un sistema software. In questi schemi si descrivono sottosistemi predefiniti insieme con i ruoli che essi assumono e le relazioni reciproche.

Pattern di metodologia

Pattern di concorrenza

Nel caso di processi che eseguono contemporaneamente delle attività su dati condivisi si parla di concorrenza. Alcuni design pattern sono stati sviluppati per mantenere sincronizzato lo stato dei dati in tali situazioni:


Il testo è disponibile secondo la licenza Creative Commons Attribuzione-Condividi allo stesso modo