Che cos'è una condizione di gara?


982

Quando si scrivono applicazioni multithread, uno dei problemi più comuni riscontrati sono le condizioni di gara.

Le mie domande alla community sono:

Qual è la condizione di gara?
Come li rilevi?
Come li gestisci?
Infine, come evitare che si verifichino?


3
C'è un grande capitolo nel HOWTO sulla Programmazione sicura per Linux che descrive cosa sono e come evitarli.
Craig H,

4
Vorrei menzionare che, senza specificare la lingua, non è possibile rispondere correttamente a molte parti di questa domanda, poiché in diverse lingue, la definizione, le conseguenze e gli strumenti per prevenirle potrebbero differire.
MikeMB,

@MikeMB. D'accordo, tranne quando si analizza l'esecuzione del codice byte, come è stato fatto da Race Catcher (vedere questa discussione stackoverflow.com/a/29361427/1363844 ) possiamo indirizzare tutte quelle circa 62 lingue che vengono compilate in codice byte (vedi en.wikipedia.org / wiki / List_of_JVM_languages )
Ben

Risposte:


1238

Una condizione di competizione si verifica quando due o più thread possono accedere ai dati condivisi e tentano di modificarli contemporaneamente. Poiché l'algoritmo di pianificazione dei thread può passare da un thread all'altro in qualsiasi momento, non si conosce l'ordine in cui i thread tenteranno di accedere ai dati condivisi. Pertanto, il risultato della modifica dei dati dipende dall'algoritmo di pianificazione dei thread, ovvero entrambi i thread stanno "correndo" per accedere / modificare i dati.

Spesso si verificano problemi quando un thread esegue un "check-then-act" (ad esempio "check" se il valore è X, quindi "act" per fare qualcosa che dipende dal valore di X) e un altro thread fa qualcosa al valore in tra "spunta" e "atto". Per esempio:

if (x == 5) // The "Check"
{
   y = x * 2; // The "Act"

   // If another thread changed x in between "if (x == 5)" and "y = x * 2" above,
   // y will not be equal to 10.
}

Il punto è che y potrebbe essere 10 o potrebbe essere qualsiasi cosa, a seconda che un altro thread abbia cambiato x tra check e act. Non hai un vero modo di sapere.

Per evitare che si verifichino condizioni di competizione, in genere è necessario bloccare i dati condivisi per garantire che solo un thread alla volta possa accedere ai dati. Ciò significherebbe qualcosa del genere:

// Obtain lock for x
if (x == 5)
{
   y = x * 2; // Now, nothing can change x until the lock is released. 
              // Therefore y = 10
}
// release lock for x

121
Cosa fa l'altro thread quando incontra il blocco? Aspetta? Errore?
Brian Ortiz,

174
Sì, l'altro thread dovrà attendere il rilascio del blocco prima di poter procedere. Ciò rende molto importante che il blocco venga rilasciato dal thread di trattenimento al termine di esso. Se non lo rilascia mai, l'altro thread attenderà indefinitamente.
Lehane,

2
@Ian In un sistema multithreading ci saranno sempre momenti in cui le risorse devono essere condivise. Dire che un approccio è negativo senza dare un'alternativa non è produttivo. Sono sempre alla ricerca di modi per migliorare e se esiste un'alternativa lo cercherò volentieri e peserò i pro ei contro.
Despertar,

2
@Despertar ... inoltre, non è necessariamente il caso in cui le risorse dovranno sempre essere condivise in un sistema a thread multipli. Ad esempio, potresti avere un array in cui è necessario elaborare ciascun elemento. È possibile partizionare l'array e disporre di un thread per ogni partizione e i thread possono svolgere il proprio lavoro in modo completamente indipendente l'uno dall'altro.
Ian Warburton,

12
Perché si verifichi una gara è sufficiente che un singolo thread tenti di modificare i dati condivisi mentre il resto dei thread può leggerli o modificarli.
SomeWittyUsername

213

Esiste una "condizione di competizione" quando il codice multithread (o altrimenti parallelo) che accede a una risorsa condivisa potrebbe farlo in modo da causare risultati imprevisti.

Prendi questo esempio:

for ( int i = 0; i < 10000000; i++ )
{
   x = x + 1; 
}

Se hai 5 thread che eseguono questo codice contemporaneamente, il valore di x NON DOVREBBE finire per essere 50.000.000. Varrebbe infatti ad ogni corsa.

Questo perché, affinché ogni thread aumenti il ​​valore di x, devono fare quanto segue: (semplificato, ovviamente)

Recupera il valore di x
Aggiungi 1 a questo valore
Memorizza questo valore su x

Qualsiasi thread può essere in qualsiasi fase di questo processo in qualsiasi momento e possono calpestarsi quando è coinvolta una risorsa condivisa. Lo stato di x può essere modificato da un altro thread durante il tempo tra x viene letto e quando viene riscritto.

Diciamo che un thread recupera il valore di x, ma non lo ha ancora memorizzato. Un altro thread può anche recuperare lo stesso valore di x (perché nessun thread lo ha ancora modificato) e quindi entrambi memorizzerebbero lo stesso valore (x + 1) in x!

Esempio:

Discussione 1: legge x, il valore è 7
Discussione 1: aggiungi 1 a x, il valore è ora 8
Discussione 2: legge x, il valore è 7
Discussione 1: memorizza 8 in x
Discussione 2: aggiunge 1 a x, il valore è ora 8
Discussione 2: memorizza 8 in x

Le condizioni di gara possono essere evitate impiegando una sorta di meccanismo di blocco prima del codice che accede alla risorsa condivisa:

for ( int i = 0; i < 10000000; i++ )
{
   //lock x
   x = x + 1; 
   //unlock x
}

Qui, la risposta esce ogni volta come 50.000.000.

Per ulteriori informazioni sul blocco, cerca: mutex, semaforo, sezione critica, risorsa condivisa.


Vedi jakob.engbloms.se/archives/65 per un esempio di un programma per testare come tali cose vanno male ... dipende davvero dal modello di memoria della macchina su cui stai girando.
jakobengblom2,

1
Come può arrivare a 50 milioni se deve fermarsi a 10 milioni?

9
@nocomprende: con 5 thread che eseguono lo stesso codice alla volta, come descritto direttamente sotto lo snippet ...
Jon Skeet,

4
@JonSkeet Hai ragione, ho confuso io e x. Grazie.

Il doppio controllo del blocco nell'implementazione del modello Singleton è un esempio di prevenzione delle condizioni di gara.
Bharat Dodeja,

150

Che cos'è una condizione di gara?

Stai pensando di andare al cinema alle 17:00. Informarsi sulla disponibilità dei biglietti alle 16:00. Il rappresentante afferma che sono disponibili. Ti rilassi e raggiungi la biglietteria 5 minuti prima dello spettacolo. Sono sicuro che puoi indovinare cosa succede: è una casa piena. Il problema qui era nella durata tra il controllo e l'azione. Hai chiesto a 4 e agito a 5. Nel frattempo, qualcun altro ha afferrato i biglietti. Questa è una condizione di gara, in particolare uno scenario "check-then-act" delle condizioni di gara.

Come li rilevi?

Revisione del codice religioso, test unitari multi-thread. Non ci sono scorciatoie. Ci sono pochi plugin di Eclipse che stanno emergendo su questo, ma nulla di ancora stabile.

Come gestirli e prevenirli?

La cosa migliore sarebbe creare funzioni gratuite e stateless con effetti collaterali, usare il più possibile immutabili. Ma ciò non è sempre possibile. Pertanto, utilizzando java.util.concurrent.atomic, le strutture di dati simultanee, la sincronizzazione corretta e la concorrenza basata sugli attori aiuteranno.

La migliore risorsa per la concorrenza è JCIP. Puoi anche ottenere ulteriori dettagli sulla spiegazione sopra qui .


Le revisioni del codice e i test unitari sono secondari per modellare il flusso tra le orecchie e fare meno uso della memoria condivisa.
Acumenus,

2
Ho apprezzato l'esempio reale di una condizione di gara
Tom O.

11
Come la risposta pollice in alto . La soluzione è: blocchi i biglietti tra 4-5 con mutex (mutua eccezione, c ++). Nel mondo reale si chiama prenotazione del biglietto :)
Volt

1
sarebbe una risposta decente se si lasciassero cadere i bit solo Java (la domanda non riguarda Java, ma piuttosto le condizioni di gara in generale)
Corey Goldberg,

No. Questa non è una condizione di gara. Dal punto di vista "commerciale" hai aspettato troppo a lungo. Ovviamente il backorder non è una soluzione. Prova uno scalper, altrimenti acquista il biglietto come assicurazione
csherriff,

65

Esiste un'importante differenza tecnica tra condizioni di gara e gare di dati. La maggior parte delle risposte sembrano supporre che questi termini siano equivalenti, ma non lo sono.

Una gara di dati si verifica quando 2 istruzioni accedono alla stessa posizione di memoria, almeno uno di questi accessi è una scrittura e non si verificano eventi prima di ordinare tra questi accessi. Ora, ciò che costituisce un accada prima dell'ordinamento è oggetto di molte discussioni, ma in generale le coppie ulock-lock sulla stessa variabile di blocco e le coppie di segnali di attesa sulla stessa variabile di condizione inducono un ordine accada prima.

Una condizione di competizione è un errore semantico. È un difetto che si verifica nella tempistica o nell'ordinamento degli eventi che porta a comportamenti errati del programma .

Molte condizioni di gara possono essere (e in effetti lo sono) causate da gare di dati, ma ciò non è necessario. È un dato di fatto, le gare di dati e le condizioni di gara non sono né le condizioni necessarie, né quelle sufficienti l'una per l'altra. Questo post sul blog spiega molto bene la differenza, con un semplice esempio di transazione bancaria. Ecco un altro semplice esempio che spiega la differenza.

Ora che abbiamo inchiodato la terminologia, proviamo a rispondere alla domanda originale.

Dato che le condizioni di gara sono bug semantici, non esiste un modo generale per rilevarli. Questo perché non c'è modo di avere un oracolo automatizzato in grado di distinguere il comportamento corretto dal programma errato nel caso generale. Il rilevamento della razza è un problema indecidibile.

D'altra parte, le razze di dati hanno una definizione precisa che non si riferisce necessariamente alla correttezza e quindi è possibile rilevarle. Esistono molti tipi di rilevatori di gare di dati (rilevamento di corse di dati statici / dinamici, rilevamento di corse di dati basato su lockset, rilevamento di corse di dati basato su eventi precedenti, rilevamento di corse di dati ibridi). Un rilevatore di corse di dati dinamici all'avanguardia è ThreadSanitizer che funziona molto bene nella pratica.

La gestione delle gare di dati in generale richiede una certa disciplina di programmazione per indurre i limiti di avvenimento prima degli accessi ai dati condivisi (durante lo sviluppo o una volta rilevati utilizzando gli strumenti sopra menzionati). questo può essere fatto attraverso blocchi, variabili di condizione, semafori, ecc. Tuttavia, si possono anche utilizzare diversi paradigmi di programmazione come il passaggio di messaggi (anziché la memoria condivisa) che evitano la corsa dei dati per costruzione.


La differenza è fondamentale per comprendere le condizioni di gara. Grazie!
ProgramCpp

37

Una definizione canonica è " quando due thread accedono contemporaneamente alla stessa posizione in memoria e almeno uno degli accessi è una scrittura ". Nella situazione il thread "reader" può ottenere il vecchio valore o il nuovo valore, a seconda di quale thread "vince la gara". Questo non è sempre un bug - in effetti, alcuni algoritmi di basso livello davvero pelosi lo fanno apposta - ma dovrebbe essere generalmente evitato. @Steve Gury è un buon esempio di quando potrebbe essere un problema.


3
Potresti per favore fare un esempio di come le condizioni di gara possano essere utili? Google non ha aiutato.
Alex V.

3
@Alex V. A questo punto, non ho idea di cosa stavo parlando. Penso che questo possa essere stato un riferimento alla programmazione senza lock, ma non è proprio esatto affermare che dipende dalle condizioni di gara, di per sé.
Chris Conway,

33

Una condizione di razza è una specie di bug, che si verifica solo con determinate condizioni temporali.

Esempio: immagina di avere due thread, A e B.

Nel thread A:

if( object.a != 0 )
    object.avg = total / object.a

Nella discussione B:

object.a = 0

Se il thread A viene annullato subito dopo aver verificato che object.a non sia nullo, B lo farà a = 0e quando il thread A otterrà il processore, eseguirà una "divisione per zero".

Questo errore si verifica solo quando il thread A viene annullato subito dopo l'istruzione if, è molto raro, ma può succedere.


21

Le condizioni di gara non sono solo legate al software ma anche all'hardware. In realtà il termine è stato inizialmente coniato dall'industria dell'hardware.

Secondo Wikipedia :

Il termine ha origine dall'idea di due segnali che si corrono per influenzare prima l'uscita .

Condizioni di gara in un circuito logico:

inserisci qui la descrizione dell'immagine

L'industria del software ha assunto questo termine senza modifiche, il che rende un po 'difficile da capire.

È necessario effettuare alcune sostituzioni per mapparlo al mondo del software:

  • "two segnali" => "due thread" / "due processi"
  • "influire sull'output" => "influenza un certo stato condiviso"

Quindi le condizioni di competizione nell'industria del software significano "due thread" / "due processi" che si corrono a vicenda per "influenzare uno stato condiviso", e il risultato finale dello stato condiviso dipenderà da una sottile differenza temporale, che potrebbe essere causata da qualche ordine di avvio thread / processo, pianificazione thread / processo, ecc.


20

Una condizione di competizione è una situazione di programmazione concorrente in cui due thread o processi simultanei competono per una risorsa e lo stato finale risultante dipende da chi ottiene la risorsa per prima.


spiegazione semplicemente geniale
gokareless

Stato finale di cosa?
Roman Alexandrovich,

1
@RomanAlexandrovich Lo stato finale del programma. Lo stato si riferisce a cose come i valori delle variabili, ecc. Vedi la risposta eccellente di Lehane. Lo "stato" nel suo esempio farebbe riferimento ai valori finali di 'x' e 'y'.
AMTerp

19

Le condizioni di gara si verificano in applicazioni multi-thread o sistemi multi-processo. Una condizione razziale, nella sua forma più elementare, è qualsiasi cosa che presupponga che due cose che non si trovano nello stesso thread o processo accadranno in un ordine particolare, senza prendere provvedimenti per assicurarsi che ciò avvenga. Ciò accade comunemente quando due thread passano messaggi impostando e controllando le variabili membro di una classe a cui entrambi possono accedere. C'è quasi sempre una condizione di competizione quando un thread chiama sleep per dare a un altro thread il tempo di terminare un'attività (a meno che quel sleep non sia in loop, con qualche meccanismo di controllo).

Gli strumenti per prevenire le condizioni di gara dipendono dalla lingua e dal sistema operativo, ma alcuni comuni sono mutex, sezioni critiche e segnali. I mutex sono buoni quando vuoi assicurarti di essere l'unico a fare qualcosa. I segnali sono buoni quando vuoi assicurarti che qualcun altro abbia finito di fare qualcosa. Ridurre al minimo le risorse condivise può anche aiutare a prevenire comportamenti imprevisti

Rilevare le condizioni di gara può essere difficile, ma ci sono un paio di segni. Il codice che si basa fortemente sul sonno è soggetto alle condizioni di gara, quindi prima controlla le chiamate a dormire nel codice interessato. L'aggiunta di sleep particolarmente lunghi può essere utilizzata anche per il debug per provare a forzare un particolare ordine di eventi. Questo può essere utile per riprodurre il comportamento, vedere se riesci a farlo scomparire cambiando i tempi delle cose e per testare le soluzioni messe in atto. I sleep dovrebbero essere rimossi dopo il debug.

Il segno distintivo che uno ha una condizione di gara, tuttavia, è se c'è un problema che si verifica solo in modo intermittente su alcune macchine. I bug comuni sarebbero crash e deadlock. Con la registrazione, dovresti essere in grado di trovare l'area interessata e tornare da lì.


10

Microsoft ha effettivamente pubblicato un articolo davvero dettagliato su questa questione di condizioni di gara e deadlock. L'estratto più riassunto da esso sarebbe il paragrafo del titolo:

Si verifica una condizione di competizione quando due thread accedono contemporaneamente a una variabile condivisa. Il primo thread legge la variabile e il secondo thread legge lo stesso valore dalla variabile. Quindi il primo thread e il secondo thread eseguono le loro operazioni sul valore e corrono per vedere quale thread può scrivere l'ultimo valore nella variabile condivisa. Il valore del thread che scrive l'ultimo valore viene conservato, poiché il thread sta scrivendo sul valore scritto dal thread precedente.


5

Che cos'è una condizione di gara?

La situazione in cui il processo dipende in modo critico dalla sequenza o dalla tempistica di altri eventi.

Ad esempio, il processore A e il processore B necessitano entrambi di risorse identiche per la loro esecuzione.

Come li rilevi?

Esistono strumenti per rilevare automaticamente le condizioni di gara:

Come li gestisci?

Le condizioni di gara possono essere gestite da Mutex o Semaphores . Agiscono come un blocco che consente a un processo di acquisire una risorsa in base a determinati requisiti per prevenire le condizioni di competizione.

Come si evita che si verifichino?

Esistono vari modi per prevenire le condizioni di gara, come Evitare la sezione critica .

  1. Non ci sono due processi contemporaneamente all'interno delle loro regioni critiche. ( Esclusione reciproca)
  2. Non vengono fatte ipotesi sulla velocità o sul numero di CPU.
  3. Nessun processo in esecuzione al di fuori della sua area critica che blocca altri processi.
  4. Nessun processo deve aspettare per sempre per entrare nella sua regione critica. (A aspetta B risorse, B aspetta C risorse, C aspetta A risorse)

2

Una condizione di competizione è una situazione indesiderabile che si verifica quando un dispositivo o un sistema tenta di eseguire due o più operazioni contemporaneamente, ma a causa della natura del dispositivo o del sistema, le operazioni devono essere eseguite nella sequenza corretta per essere fatto correttamente.

Nella memoria o nella memoria del computer, può verificarsi una condizione di competizione se i comandi per leggere e scrivere una grande quantità di dati vengono ricevuti quasi nello stesso istante e la macchina tenta di sovrascrivere alcuni o tutti i vecchi dati mentre i vecchi dati sono ancora leggere. Il risultato può essere uno o più dei seguenti: un arresto anomalo del computer, una "operazione illegale", la notifica e l'arresto del programma, errori nella lettura dei vecchi dati o errori nella scrittura dei nuovi dati.


2

Ecco il classico esempio di saldo del conto bancario che aiuterà i neofiti a comprendere i thread in Java facilmente in grado di gestire le condizioni di gara:

public class BankAccount {

/**
 * @param args
 */
int accountNumber;
double accountBalance;

public synchronized boolean Deposit(double amount){
    double newAccountBalance=0;
    if(amount<=0){
        return false;
    }
    else {
        newAccountBalance = accountBalance+amount;
        accountBalance=newAccountBalance;
        return true;
    }

}
public synchronized boolean Withdraw(double amount){
    double newAccountBalance=0;
    if(amount>accountBalance){
        return false;
    }
    else{
        newAccountBalance = accountBalance-amount;
        accountBalance=newAccountBalance;
        return true;
    }
}

public static void main(String[] args) {
    // TODO Auto-generated method stub
    BankAccount b = new BankAccount();
    b.accountBalance=2000;
    System.out.println(b.Withdraw(3000));

}

1

Puoi prevenire le condizioni di gara se usi le classi "Atomic". Il motivo è solo che il thread non separa l'operazione get and set, l'esempio è di seguito:

AtomicInteger ai = new AtomicInteger(2);
ai.getAndAdd(5);

Di conseguenza, avrai 7 nel link "ai". Sebbene tu abbia fatto due azioni, ma entrambe le operazioni confermano lo stesso thread e nessun altro thread interferirà con questo, ciò significa che non ci sono condizioni di gara!


0

Prova questo esempio di base per una migliore comprensione delle condizioni di gara:

    public class ThreadRaceCondition {

    /**
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        Account myAccount = new Account(22222222);

        // Expected deposit: 250
        for (int i = 0; i < 50; i++) {
            Transaction t = new Transaction(myAccount,
                    Transaction.TransactionType.DEPOSIT, 5.00);
            t.start();
        }

        // Expected withdrawal: 50
        for (int i = 0; i < 50; i++) {
            Transaction t = new Transaction(myAccount,
                    Transaction.TransactionType.WITHDRAW, 1.00);
            t.start();

        }

        // Temporary sleep to ensure all threads are completed. Don't use in
        // realworld :-)
        Thread.sleep(1000);
        // Expected account balance is 200
        System.out.println("Final Account Balance: "
                + myAccount.getAccountBalance());

    }

}

class Transaction extends Thread {

    public static enum TransactionType {
        DEPOSIT(1), WITHDRAW(2);

        private int value;

        private TransactionType(int value) {
            this.value = value;
        }

        public int getValue() {
            return value;
        }
    };

    private TransactionType transactionType;
    private Account account;
    private double amount;

    /*
     * If transactionType == 1, deposit else if transactionType == 2 withdraw
     */
    public Transaction(Account account, TransactionType transactionType,
            double amount) {
        this.transactionType = transactionType;
        this.account = account;
        this.amount = amount;
    }

    public void run() {
        switch (this.transactionType) {
        case DEPOSIT:
            deposit();
            printBalance();
            break;
        case WITHDRAW:
            withdraw();
            printBalance();
            break;
        default:
            System.out.println("NOT A VALID TRANSACTION");
        }
        ;
    }

    public void deposit() {
        this.account.deposit(this.amount);
    }

    public void withdraw() {
        this.account.withdraw(amount);
    }

    public void printBalance() {
        System.out.println(Thread.currentThread().getName()
                + " : TransactionType: " + this.transactionType + ", Amount: "
                + this.amount);
        System.out.println("Account Balance: "
                + this.account.getAccountBalance());
    }
}

class Account {
    private int accountNumber;
    private double accountBalance;

    public int getAccountNumber() {
        return accountNumber;
    }

    public double getAccountBalance() {
        return accountBalance;
    }

    public Account(int accountNumber) {
        this.accountNumber = accountNumber;
    }

    // If this method is not synchronized, you will see race condition on
    // Remove syncronized keyword to see race condition
    public synchronized boolean deposit(double amount) {
        if (amount < 0) {
            return false;
        } else {
            accountBalance = accountBalance + amount;
            return true;
        }
    }

    // If this method is not synchronized, you will see race condition on
    // Remove syncronized keyword to see race condition
    public synchronized boolean withdraw(double amount) {
        if (amount > accountBalance) {
            return false;
        } else {
            accountBalance = accountBalance - amount;
            return true;
        }
    }
}

0

Non sempre vuoi scartare una condizione di gara. Se hai un flag che può essere letto e scritto da più thread e questo flag è impostato su "completato" da un thread in modo che l'altro thread interrompa l'elaborazione quando il flag è impostato su "completato", non vuoi quella "razza" condizione "da eliminare. In effetti, questa può essere definita una condizione di razza benigna.

Tuttavia, utilizzando uno strumento per il rilevamento delle condizioni di gara, verrà individuato come una condizione di competizione dannosa.

Maggiori dettagli sulle condizioni di gara qui, http://msdn.microsoft.com/en-us/magazine/cc546569.aspx .


In quale lingua si basa la tua risposta?
MikeMB,

Francamente mi sembra che se hai le condizioni di gara in , non stai progettando il tuo codice in modo strettamente controllato. Che, sebbene potrebbe non essere un problema nel tuo caso teorico, è la prova di problemi più grandi con il modo in cui progetti e sviluppi software. Aspettatevi di affrontare dolorosi bug di razza prima o poi.
Ingegnere,

0

Si consideri un'operazione che deve visualizzare il conteggio non appena il conteggio viene incrementato. cioè, non appena CounterThread aumenta il valore DisplayThread deve visualizzare il valore aggiornato di recente.

int i = 0;

Produzione

CounterThread -> i = 1  
DisplayThread -> i = 1  
CounterThread -> i = 2  
CounterThread -> i = 3  
CounterThread -> i = 4  
DisplayThread -> i = 4

Qui CounterThread ottiene frequentemente il blocco e aggiorna il valore prima che DisplayThread lo visualizzi. Qui esiste una condizione di razza. Le condizioni di gara possono essere risolte utilizzando la sincronizzazione


0

Una race condition è una situazione indesiderata che si verifica quando due o più processi possono accedere e modificare i dati condivisi contemporaneamente. Si è verificato a causa di accessi contrastanti a una risorsa. Un problema di sezione critica può causare condizioni di gara. Per risolvere le condizioni critiche nel processo abbiamo eliminato solo un processo alla volta che esegue la sezione critica.

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.