Quando dovrei usare Lazy <T>?


327

Ho trovato questo articolo su Lazy: Pigrizia in C # 4.0 - Lazy

Qual è la migliore pratica per ottenere le migliori prestazioni usando oggetti pigri? Qualcuno può indicarmi un uso pratico in una vera applicazione? In altre parole, quando dovrei usarlo?


42
Sostituisce: get { if (foo == null) foo = new Foo(); return foo; }. E ci sono miliardi di posti possibili per usarlo ...
Kirk Woll,

57
Si noti che get { if (foo == null) foo = new Foo(); return foo; }non è thread-safe, mentre Lazy<T>è thread-safe per impostazione predefinita.
Matteo,

23
Da MSDN: IMPORTANTE: l'inizializzazione lazy è thread-safe, ma non protegge l'oggetto dopo la creazione. È necessario bloccare l'oggetto prima di accedervi, a meno che il tipo non sia thread-safe.
Pedro.The.Kid

Risposte:


237

Di solito lo usi quando vuoi creare un'istanza di qualcosa la prima volta che viene effettivamente utilizzato. Ciò ritarda il costo della sua creazione fino a quando / quando è necessario invece di sostenere sempre il costo.

Di solito questo è preferibile quando l'oggetto può o non può essere usato e il costo della sua costruzione non è banale.


121
perché non usare SEMPRE Lazy?
TruthOf42

44
Sostiene il costo al primo utilizzo e può utilizzare un certo sovraccarico di blocco (o sacrificare la sicurezza del thread in caso contrario) per farlo. Pertanto, dovrebbe essere scelto con cura e non utilizzato se non necessario.
James Michael Hare,

3
James, potresti ampliare "e il costo di costruzione non è banale"? Nel mio caso ho 19 proprietà nella mia classe e nella maggior parte dei casi sarà necessario esaminarne solo 2 o 3. Pertanto sto prendendo in considerazione l'implementazione di ogni proprietà utilizzando Lazy<T>. Tuttavia, per creare ogni proprietà sto facendo un'interpolazione lineare (o un'interpolazione bilineare) che è abbastanza banale ma ha qualche costo. (Mi suggerirai di andare a fare il mio esperimento?)
Ben

3
James, seguendo il mio consiglio, ho fatto il mio esperimento. Vedi il mio post .
Ben

17
È possibile che si desideri inizializzare / creare un'istanza di tutto "durante" l'avvio del sistema per impedire la latenza dell'utente in sistemi con throughput elevato e bassa latenza. Questo è solo uno dei tanti motivi per non "usare sempre" Lazy.
Derrick,

126

Dovresti cercare di evitare l'uso di Singleton, ma se mai dovessi farlo, Lazy<T>semplifica l'implementazione di singlet pigri e sicuri per i thread:

public sealed class Singleton
{
    // Because Singleton's constructor is private, we must explicitly
    // give the Lazy<Singleton> a delegate for creating the Singleton.
    static readonly Lazy<Singleton> instanceHolder =
        new Lazy<Singleton>(() => new Singleton());

    Singleton()
    {
        // Explicit private constructor to prevent default public constructor.
        ...
    }

    public static Singleton Instance => instanceHolder.Value;
}

38
Odio leggere Dovresti cercare di evitare di usare Singleton quando li sto usando: D ... ora devo imparare perché dovrei cercare di evitarli: D
Bart Calixto

24
Smetterò di usare Singletons quando Microsoft smetterà di usarli nei loro esempi.
eaglei22,

4
Tendo a non essere d'accordo con l'idea di dover evitare Singletons. Quando si segue il paradigma dell'iniezione di dipendenza, non dovrebbe importare in nessun modo. Idealmente, tutte le dipendenze dovrebbero essere create una sola volta. Ciò riduce la pressione sul GC in scenari di carico elevato. Pertanto, farli diventare un Singleton all'interno della classe stessa va bene. La maggior parte (se non tutti) i moderni contenitori DI possono gestirlo in qualsiasi modo tu scelga.
Lee Grissom,

1
Non è necessario utilizzare un modello singleton come quello, invece utilizzare qualsiasi contenitore per configurare la classe per singleton. Il contenitore si prenderà cura del sovraccarico per te.
VivekDev l'

Tutto ha uno scopo, ci sono situazioni in cui i singoli sono un buon approccio e situazioni in cui non lo è :).
Hawkzey,

86

Un ottimo esempio del mondo reale in cui il caricamento lento è utile è con ORM (Object Relation Mapper) come Entity Framework e NHibernate.

Supponi di avere un'entità Cliente che ha proprietà per Nome, Numero di telefono e Ordini. Nome e PhoneNumber sono stringhe regolari, ma Orders è una proprietà di navigazione che restituisce un elenco di tutti gli ordini che il cliente abbia mai effettuato.

Spesso potresti voler esaminare tutti i tuoi clienti e ottenere il loro nome e numero di telefono per chiamarli. Questo è un compito molto semplice e veloce, ma immagina se ogni volta che creavi un cliente andasse automaticamente e facesse un join complesso per restituire migliaia di ordini. La parte peggiore è che non hai nemmeno intenzione di utilizzare gli ordini, quindi è uno spreco di risorse completo!

Questo è il luogo perfetto per il caricamento lazy perché se la proprietà Order è lazy non andrà a prendere tutto l'ordine del cliente a meno che non sia effettivamente necessario. Puoi enumerare gli oggetti Cliente ottenendo solo il loro nome e numero di telefono mentre la proprietà Ordine dorme pazientemente, pronta per quando ne hai bisogno.


34
Cattivo esempio, poiché tale caricamento lento di solito è già incorporato nell'ORM. Non dovresti iniziare ad aggiungere valori Lazy <T> ai tuoi POCO per ottenere un caricamento pigro, ma usa il modo specifico ORM per farlo.
Dynalon,

56
@Dyna Questo esempio si riferisce al caricamento lento incorporato di un ORM perché penso che ciò esemplifichi l'utilità del caricamento lento in modo chiaro e semplice.
Despertar

Quindi, se si utilizza Entity Framework, si dovrebbe imporre il proprio pigro? O EF lo fa per te?
Zapnologica,

7
@Zapnologica EF fa tutto questo per impostazione predefinita. In effetti, se si desidera un caricamento desideroso (l'opposto del caricamento lento), è necessario indicare esplicitamente EF utilizzando Db.Customers.Include("Orders"). Ciò causerà l'esecuzione del join dell'ordine in quel momento anziché quando la Customer.Ordersproprietà viene utilizzata per la prima volta. Il caricamento lento può anche essere disabilitato tramite DbContext.
Despertar,

2
In realtà questo è un buon esempio, in quanto si potrebbe desiderare di aggiungere questa funzionalità quando si utilizza qualcosa come Dapper.
tbone,

41

Ho preso in considerazione l'utilizzo delle Lazy<T>proprietà per migliorare le prestazioni del mio codice (e per saperne di più su di esso). Sono venuto qui alla ricerca di risposte su quando usarlo ma sembra che ovunque io vada ci siano frasi come:

Utilizzare l'inizializzazione lenta per rinviare la creazione di un oggetto di grandi dimensioni o ad alta intensità di risorse o l'esecuzione di un'attività ad alta intensità di risorse, in particolare quando tale creazione o esecuzione potrebbe non verificarsi durante la durata del programma.

da MSDN Classe <T> pigra

Sono un po 'confuso perché non sono sicuro di dove disegnare la linea. Ad esempio, considero l'interpolazione lineare come un calcolo abbastanza veloce ma se non ho bisogno di farlo, l'inizializzazione pigra può aiutarmi a evitare di farlo e ne vale la pena?

Alla fine ho deciso di provare il mio test e ho pensato di condividere i risultati qui. Sfortunatamente non sono davvero un esperto nel fare questo tipo di test e quindi sono felice di ricevere commenti che suggeriscono miglioramenti.

Descrizione

Nel mio caso, ero particolarmente interessato a vedere se Lazy Properties potesse aiutare a migliorare una parte del mio codice che fa molta interpolazione (la maggior parte essendo inutilizzata) e quindi ho creato un test che ha confrontato 3 approcci.

Ho creato una classe di test separata con 20 proprietà di test (chiamiamole t-properties) per ogni approccio.

  • Classe GetInterp: esegue l'interpolazione lineare ogni volta che viene ottenuta una proprietà t.
  • Classe InitInterp: inizializza le proprietà t eseguendo l'interpolazione lineare per ognuna nel costruttore. Il get restituisce solo un doppio.
  • Classe InitLazy: imposta le proprietà t come proprietà Lazy in modo che l'interpolazione lineare venga eseguita una volta quando viene acquisita la proprietà. I get successivi dovrebbero semplicemente restituire un doppio già calcolato.

I risultati del test sono misurati in ms e sono la media di 50 istanze o 20 proprietà ottenute. Ogni test è stato quindi eseguito 5 volte.

Risultati del test 1: istanziazione (media di 50 istanze)

Class      1        2        3        4        5        Avg       %
------------------------------------------------------------------------
GetInterp  0.005668 0.005722 0.006704 0.006652 0.005572 0.0060636 6.72
InitInterp 0.08481  0.084908 0.099328 0.098626 0.083774 0.0902892 100.00
InitLazy   0.058436 0.05891  0.068046 0.068108 0.060648 0.0628296 69.59

Risultati del test 2: First Get (media di 20 proprietà)

Class      1        2        3        4        5        Avg       %
------------------------------------------------------------------------
GetInterp  0.263    0.268725 0.31373  0.263745 0.279675 0.277775 54.38
InitInterp 0.16316  0.161845 0.18675  0.163535 0.173625 0.169783 33.24
InitLazy   0.46932  0.55299  0.54726  0.47878  0.505635 0.510797 100.00

Risultati del test 3: Second Get (media di 20 proprietà)

Class      1        2        3        4        5        Avg       %
------------------------------------------------------------------------
GetInterp  0.08184  0.129325 0.112035 0.097575 0.098695 0.103894 85.30
InitInterp 0.102755 0.128865 0.111335 0.10137  0.106045 0.110074 90.37
InitLazy   0.19603  0.105715 0.107975 0.10034  0.098935 0.121799 100.00

osservazioni

GetInterpè più veloce da istanziare come previsto perché non sta facendo nulla. InitLazyè più veloce da istanziare rispetto a InitInterpsuggerire che l'overhead nell'impostazione delle proprietà pigre è più veloce del mio calcolo di interpolazione lineare. Tuttavia, sono un po 'confuso qui perché InitInterpdovrebbero fare 20 interpolazioni lineari (per impostarne le proprietà t) ma ci GetInterpvogliono solo 0,09 ms per istanziare (test 1), rispetto al quale ci vogliono 0,28 ms per fare solo un'interpolazione lineare la prima volta (test 2) e 0,1 ms per farlo la seconda volta (test 3).

Ci vuole InitLazyquasi 2 volte di più rispetto GetInterpa ottenere una proprietà la prima volta, mentre InitInterpè la più veloce, perché ha popolato le sue proprietà durante l'istanza. (Almeno questo è ciò che avrebbe dovuto fare, ma perché il suo risultato di istanza è stato molto più rapido di una singola interpolazione lineare? Quando sta esattamente facendo queste interpolazioni?)

Sfortunatamente sembra che ci sia qualche ottimizzazione automatica del codice in corso nei miei test. Dovrebbe impiegare GetInterplo stesso tempo per ottenere una proprietà la prima volta come fa la seconda volta, ma sta mostrando come più di 2 volte più veloce. Sembra che questa ottimizzazione influisca anche sulle altre classi, dal momento che tutte impiegano la stessa quantità di tempo per il test 3. Tuttavia, tali ottimizzazioni possono anche aver luogo nel mio codice di produzione, che può anche essere una considerazione importante.

conclusioni

Mentre alcuni risultati sono come previsto, ci sono anche alcuni risultati inaspettati molto interessanti probabilmente dovuti all'ottimizzazione del codice. Anche per le classi che sembrano fare molto lavoro nel costruttore, i risultati dell'istanza mostrano che potrebbero essere ancora molto veloci da creare, rispetto all'ottenimento di una doppia proprietà. Mentre gli esperti in questo campo possono essere in grado di commentare e indagare più approfonditamente, la mia sensazione personale è che devo ripetere questo test, ma sul mio codice di produzione per esaminare quale tipo di ottimizzazioni potrebbero aver luogo anche lì. Tuttavia, mi aspetto che InitInterppotrebbe essere la strada da percorrere.


26
forse dovresti pubblicare il tuo codice di prova per riprodurre il tuo output, perché senza conoscere il tuo codice sarà difficile suggerire nulla
WiiMaxx,

1
Credo che il principale compromesso sia tra l'utilizzo della memoria (pigro) e l'uso della CPU (non pigro). Perché lazydeve fare un po 'di contabilità extra, InitLazyuserebbe più memoria rispetto alle altre soluzioni. Potrebbe anche avere un impatto minore sulle prestazioni ad ogni accesso, mentre controlla se ha già un valore o meno; trucchi intelligenti potrebbero rimuovere quel sovraccarico, ma richiederebbe un supporto speciale in IL. (Haskell lo fa trasformando ogni valore pigro in una chiamata di funzione; una volta generato il valore, viene sostituito con una funzione che restituisce quel valore ogni volta.)
jpaugh,

14

Giusto per indicare l'esempio pubblicato da Mathew

public sealed class Singleton
{
    // Because Singleton's constructor is private, we must explicitly
    // give the Lazy<Singleton> a delegate for creating the Singleton.
    private static readonly Lazy<Singleton> instanceHolder =
        new Lazy<Singleton>(() => new Singleton());

    private Singleton()
    {
        ...
    }

    public static Singleton Instance
    {
        get { return instanceHolder.Value; }
    }
}

prima della nascita del Pigro l'avremmo fatto in questo modo:

private static object lockingObject = new object();
public static LazySample InstanceCreation()
{
    if(lazilyInitObject == null)
    {
         lock (lockingObject)
         {
              if(lazilyInitObject == null)
              {
                   lazilyInitObject = new LazySample ();
              }
         }
    }
    return lazilyInitObject ;
}

6
Per questo uso sempre un contenitore IoC.
Jowen,

1
Sono pienamente d'accordo sul considerare un contenitore IoC per questo. Se tuttavia vuoi un semplice oggetto inizializzato pigro, considera anche che se non hai bisogno di questo è sicuro per i thread, farlo manualmente con un If può essere meglio considerando l'overhead delle prestazioni di come Lazy si gestisce da solo.
Thulani Chivandikwa,

12

Da MSDN:

Utilizzare un'istanza di Lazy per rinviare la creazione di un oggetto grande o ad alta intensità di risorse o l'esecuzione di un'attività ad alta intensità di risorse, in particolare quando tale creazione o esecuzione potrebbe non verificarsi durante la durata del programma.

Oltre alla risposta di James Michael Hare, Lazy offre un'inizializzazione thread-safe del tuo valore. Dai un'occhiata alla voce MSDN di enumerazione LazyThreadSafetyMode che descrive vari tipi di modalità di sicurezza del thread per questa classe.


-2

Dovresti guardare questo esempio per capire l'architettura di caricamento lento

private readonly Lazy<List<int>> list = new Lazy<List<int>>(() =>
{
    List<int> configList = new List<int>(Thread.CurrentThread.ManagedThreadId);
    return configList;
});
public void Execute()
{
    list.Value.Add(0);
    if (list.IsValueCreated)
    {
        list.Value.Add(1);
        list.Value.Add(2);

        foreach (var item in list.Value)
        {
            Console.WriteLine(item);
        }
    }
    else
    {
        Console.WriteLine("Value not created");
    }
}

-> output -> 0 1 2

ma se questo codice non scrive "list.Value.Add (0);"

output -> Valore non creato

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.