Domanda

Mi è stato insegnato a credere che se più thread possono accedere a una variabile, tutte le letture e le scritture su quella variabile devono essere protette da un codice di sincronizzazione, come un'istruzione "lock", perché il processore potrebbe passare a un altro thread a metà uno scritto.

Tuttavia, stavo esaminando System.Web.Security.Membership utilizzando Reflector e ho trovato un codice come questo:

public static class Membership
{
    private static bool s_Initialized = false;
    private static object s_lock = new object();
    private static MembershipProvider s_Provider;

    public static MembershipProvider Provider
    {
        get
        {
            Initialize();
            return s_Provider;
        }
    }

    private static void Initialize()
    {
        if (s_Initialized)
            return;

        lock(s_lock)
        {
            if (s_Initialized)
                return;

            // Perform initialization...
            s_Initialized = true;
        }
    }
}

Perché il campo s_Initialized viene letto all'esterno del blocco?Non è possibile che un altro thread stia tentando di scrivervi contemporaneamente? Le letture e le scritture di variabili sono atomiche?

È stato utile?

Soluzione

Per la risposta definitiva vai alle specifiche.:)

La Partizione I, Sezione 12.6.6 delle specifiche CLI afferma:"Una CLI conforme deve garantire che l'accesso in lettura e scrittura a posizioni di memoria correttamente allineate non più grandi della dimensione della parola nativa sia atomico quando tutti gli accessi in scrittura a una posizione hanno la stessa dimensione."

Ciò conferma che s_Initialized non sarà mai instabile e che la lettura e la scrittura su tipi primitivi inferiori a 32 bit sono atomiche.

In particolare, double E long (Int64 E UInt64) Sono non garantito per essere atomico su una piattaforma a 32 bit.È possibile utilizzare i metodi su Interlocked classe per proteggerli.

Inoltre, sebbene le letture e le scritture siano atomiche, esiste una condizione di competizione con i tipi primitivi di addizione, sottrazione e incremento e decremento, poiché devono essere letti, gestiti e riscritti.La classe interbloccata consente di proteggerli utilizzando il file CompareExchange E Increment metodi.

L'interblocco crea una barriera di memoria per impedire al processore di riordinare letture e scritture.Il lucchetto crea l'unica barriera richiesta in questo esempio.

Altri suggerimenti

Questa è una (cattiva) forma del modello di blocco del doppio controllo che è non thread-safe in C#!

C'è un grosso problema in questo codice:

s_Initialized non è volatile.Ciò significa che le scritture nel codice di inizializzazione possono spostarsi dopo che s_Initialized è impostato su true e altri thread possono vedere il codice non inizializzato anche se s_Initialized è true per loro.Ciò non si applica all'implementazione del Framework da parte di Microsoft perché ogni scrittura è una scrittura volatile.

Ma anche nell'implementazione di Microsoft, le letture dei dati non inizializzati possono essere riordinate (ad es.precaricati dalla CPU), quindi se s_Initialized è vero, la lettura dei dati che dovrebbero essere inizializzati può comportare la lettura di dati vecchi e non inizializzati a causa di riscontri nella cache (ad es.le letture vengono riordinate).

Per esempio:

Thread 1 reads s_Provider (which is null)  
Thread 2 initializes the data  
Thread 2 sets s\_Initialized to true  
Thread 1 reads s\_Initialized (which is true now)  
Thread 1 uses the previously read Provider and gets a NullReferenceException

Spostare la lettura di s_Provider prima della lettura di s_Initialized è perfettamente legale perché non esiste alcuna lettura volatile da nessuna parte.

Se s_Initialized fosse volatile, la lettura di s_Provider non potrebbe spostarsi prima della lettura di s_Initialized e anche l'inizializzazione del Provider non potrebbe spostarsi dopo che s_Initialized è impostato su true e ora è tutto ok.

Anche Joe Duffy ha scritto un articolo su questo problema: Varianti rotte sulla chiusura a doppio controllo

Aspetta: la domanda che c'è nel titolo non è sicuramente la vera domanda che Rory sta facendo.

La domanda principale ha la semplice risposta "No" - ma questo non è di alcun aiuto, se si vede la vera domanda - alla quale non credo che nessuno abbia dato una risposta semplice.

La vera domanda posta da Rory viene presentata molto più tardi ed è più pertinente all'esempio che fornisce.

Perché il campo S_inizializzato viene letto al di fuori del blocco?

Anche la risposta a questa domanda è semplice, sebbene del tutto estranea all’atomicità dell’accesso alle variabili.

Il campo s_Initialized viene letto all'esterno del blocco perché le serrature sono costose.

Poiché il campo s_Initialized è essenzialmente "scrivi una volta", non restituirà mai un falso positivo.

È economico leggerlo fuori dalla serratura.

Questo è un basso costo attività con a alto possibilità di avere un beneficio.

Ecco perché viene letto al di fuori della serratura: per evitare di pagare il costo dell'utilizzo di una serratura a meno che non sia indicato.

Se le serrature fossero economiche, il codice sarebbe più semplice e ometterebbe il primo controllo.

(modificare:segue la bella risposta di Rory.Sì, le letture booleane sono molto atomiche.Se qualcuno costruisse un processore con letture booleane non atomiche, verrebbe presentato sul DailyWTF.)

La risposta corretta sembra essere: "Sì, soprattutto".

  1. La risposta di John che fa riferimento alle specifiche CLI indica che gli accessi a variabili non più grandi di 32 bit su un processore a 32 bit sono atomici.
  2. Ulteriore conferma dalle specifiche C#, sezione 5.5, Atomicità dei riferimenti variabili:

    Le letture e le scritture dei seguenti tipi di dati sono atomiche:tipi bool, char, byte, sbyte, short, ushort, uint, int, float e riferimento.Inoltre, anche le letture e le scritture di tipi enum con un tipo sottostante nell'elenco precedente sono atomiche.Non è garantito che le letture e le scritture di altri tipi, inclusi long, ulong, double e decimal, nonché i tipi definiti dall'utente, siano atomiche.

  3. Il codice nel mio esempio è stato parafrasato dalla classe Membership, come scritto dallo stesso team ASP.NET, quindi era sempre lecito ritenere che il modo in cui accede al campo s_Initialized fosse corretto.Ora sappiamo perché.

Modificare:Come sottolinea Thomas Danecker, anche se l'accesso al campo è atomico, s_Initialized dovrebbe in realtà essere contrassegnato volatile per assicurarsi che il blocco non venga interrotto dal processore che riordina le letture e le scritture.

La funzione Inizializza è difettosa.Dovrebbe assomigliare più a questo:

private static void Initialize()
{
    if(s_initialized)
        return;

    lock(s_lock)
    {
        if(s_Initialized)
            return;
        s_Initialized = true;
    }
}

Senza il secondo controllo all'interno della serratura è possibile che il codice di inizializzazione venga eseguito due volte.Quindi il primo controllo riguarda le prestazioni per evitare di prendere un blocco inutilmente, e il secondo controllo riguarda il caso in cui un thread sta eseguendo il codice di inizializzazione ma non ha ancora impostato il valore s_Initialized flag e quindi un secondo thread supererebbe il primo controllo e aspetterebbe al lock.

Le letture e le scritture di variabili non sono atomiche.È necessario utilizzare le API di sincronizzazione per emulare letture/scritture atomiche.

Per un fantastico riferimento su questo e molti altri problemi legati alla concorrenza, assicurati di prendere una copia di Joe Duffy ultimo spettacolo.È uno squartatore!

"L'accesso a una variabile in C# è un'operazione atomica?"

No.E non è una cosa del C#, e nemmeno una cosa del .net, è una cosa del processore.

OJ ha capito bene che Joe Duffy è la persona a cui rivolgersi per questo tipo di informazioni.E "interbloccato" è un ottimo termine di ricerca da utilizzare se vuoi saperne di più.

"Letture incomplete" possono verificarsi su qualsiasi valore la cui somma dei campi supera la dimensione di un puntatore.

@Leone
Capisco il tuo punto: nel modo in cui l'ho chiesto e poi commentato, la domanda consente di essere interpretata in un paio di modi diversi.

Per essere chiari, volevo sapere se era sicuro che thread simultanei leggessero e scrivessero su un campo booleano senza alcun codice di sincronizzazione esplicito, ovvero accedendo a una variabile booleana (o altra variabile di tipo primitivo) atomic.

Ho quindi utilizzato il codice di appartenenza per fornire un esempio concreto, ma questo ha introdotto una serie di distrazioni, come il doppio controllo del blocco, il fatto che s_Initialized viene impostato solo una volta e che ho commentato il codice di inizializzazione stesso.

Colpa mia.

Potresti anche decorare s_Initialized con la parola chiave volatile e rinunciare completamente all'uso di lock.

Questo non è corretto.Incontrerai comunque il problema di un secondo thread che supera il controllo prima che il primo thread abbia avuto la possibilità di impostare il flag, il che risulterà in più esecuzioni del codice di inizializzazione.

Penso che ti stai chiedendo se s_Initialized potrebbe trovarsi in uno stato instabile se letto all'esterno della serratura.La risposta breve è no.Una semplice assegnazione/lettura si ridurrà a una singola istruzione di assemblaggio che è atomica su ogni processore a cui riesco a pensare.

Non sono sicuro di quale sia il caso dell'assegnazione a variabili a 64 bit, dipende dal processore, suppongo che non sia atomico ma probabilmente lo è sui moderni processori a 32 bit e certamente su tutti i processori a 64 bit.L'assegnazione di tipi di valore complessi non sarà atomica.

Pensavo che lo fossero - non sono sicuro dello scopo del blocco nel tuo esempio a meno che tu non stia facendo qualcosa anche a s_Provider allo stesso tempo - quindi il blocco garantirebbe che queste chiamate avvengano insieme.

Lo fa //Perform initialization creazione della copertina del commento s_Provider?Ad esempio

private static void Initialize()
{
    if (s_Initialized)
        return;

    lock(s_lock)
    {
        s_Provider = new MembershipProvider ( ... )
        s_Initialized = true;
    }
}

Altrimenti quella proprietà statica-get restituirà comunque null.

Forse Interbloccato dà un indizio.E altrimenti Questo sono abbastanza bravo.

Avrei immaginato che non fossero atomici.

Per fare in modo che il tuo codice funzioni sempre su architetture debolmente ordinate, devi inserire una MemoryBarrier prima di scrivere s_Initialized.

s_Provider = new MemershipProvider;

// MUST PUT BARRIER HERE to make sure the memory writes from the assignment
// and the constructor have been wriitten to memory
// BEFORE the write to s_Initialized!
Thread.MemoryBarrier();

// Now that we've guaranteed that the writes above
// will be globally first, set the flag
s_Initialized = true;

Non è garantito che le scritture di memoria che avvengono nel costruttore MembershipProvider e la scrittura su s_Provider avvengano prima della scrittura su s_Initialized su un processore con ordine debole.

Molte riflessioni in questo thread riguardano se qualcosa è atomico o meno.Non è questo il problema.Il problema è l'ordine in cui le scritture del tuo thread sono visibili ad altri thread.Su architetture debolmente ordinate, le scritture in memoria non avvengono in ordine e QUESTO è il vero problema, non se una variabile si adatta al bus dati.

MODIFICARE: In realtà, sto mescolando piattaforme nelle mie dichiarazioni.In C# le specifiche CLR richiedono che le scritture siano visibili a livello globale, in ordine (utilizzando costose istruzioni di archivio per ogni archivio, se necessario).Pertanto, non è necessario che effettivamente ci sia quella barriera della memoria lì.Tuttavia, se fosse C o C++ dove non esiste tale garanzia di ordine di visibilità globale e la tua piattaforma di destinazione potrebbe avere una memoria debolmente ordinata ed è multithread, allora dovresti assicurarti che le scritture dei costruttori siano visibili a livello globale prima di aggiornare s_Initialized , che viene testato fuori dalla serratura.

UN If (itisso) { Verificare che un booleano è atomico, ma anche se non è stato necessario bloccare il primo controllo.

Se qualche thread ha completato l'inizializzazione, allora sarà vero.Non importa se più thread controllano contemporaneamente.Riceveranno tutti la stessa risposta e non ci sarà alcun conflitto.

Il secondo controllo all'interno del lock è necessario perché un altro thread potrebbe aver prima afferrato il lock e aver già completato il processo di inizializzazione.

Quello che stai chiedendo è se accedere a un campo in un metodo più volte atomico, a cui la risposta è no.

Nell'esempio precedente, la routine di inizializzazione è errata poiché potrebbe comportare inizializzazioni multiple.Dovresti controllare il s_Initialized flag sia all'interno del blocco che all'esterno, per impedire una condizione di competizione in cui più thread leggono il file s_Initialized flag prima che qualcuno di loro esegua effettivamente il codice di inizializzazione.Per esempio.,

private static void Initialize()
{
    if (s_Initialized)
        return;

    lock(s_lock)
    {
        if (s_Initialized)
            return;
        s_Provider = new MembershipProvider ( ... )
        s_Initialized = true;
    }
}

Ack, non importa...come sottolineato, questo è effettivamente errato.Non impedisce a un secondo thread di accedere alla sezione di codice "inizializza".Bah.

Potresti anche decorare s_Initialized con la parola chiave volatile e rinunciare completamente all'uso di lock.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top