Kiedy powinienem używać Lazy <T>?

327

Znalazłem ten artykuł na temat Lazy: Lenistwo w C # 4.0 - Leniwy

Jaka jest najlepsza praktyka, aby uzyskać najlepszą wydajność przy użyciu obiektów Lazy? Czy ktoś może wskazać mi praktyczne zastosowanie w prawdziwej aplikacji? Innymi słowy, kiedy powinienem go użyć?

danyolgiax
źródło
42
Zastępuje: get { if (foo == null) foo = new Foo(); return foo; }. I jest zillions możliwych miejsc, aby z niego skorzystać ...
Kirk Woll
57
Zauważ, że get { if (foo == null) foo = new Foo(); return foo; }nie jest bezpieczny dla wątków, podczas gdy Lazy<T>domyślnie jest bezpieczny dla wątków.
Matthew
23
Z MSDN: WAŻNE: Leniwa inicjalizacja jest bezpieczna dla wątków, ale nie chroni obiektu po utworzeniu. Przed zablokowaniem obiektu należy go zablokować, chyba że typ jest bezpieczny dla wątków.
Pedro.The.Kid

Odpowiedzi:

237

Zazwyczaj używasz go, gdy chcesz utworzyć instancję czegoś przy pierwszym użyciu. Opóźnia to koszt tworzenia go do momentu, gdy jest potrzebny, zamiast zawsze ponosić koszty.

Zwykle jest to korzystne, gdy obiekt może być używany lub nie, a koszt jego budowy nie jest trywialny.

James Michael Hare
źródło
121
dlaczego nie ZAWSZE korzystasz z Lazy?
TruthOf42
44
To wiąże się z kosztami przy pierwszym użyciu i może użyć do tego pewnych blokujących narzutów (lub poświęca bezpieczeństwo nici, jeśli nie). Dlatego należy go wybierać ostrożnie i nie używać, chyba że jest to konieczne.
James Michael Hare,
3
James, czy mógłbyś rozwinąć „a koszt budowy nie jest trywialny”? W moim przypadku mam 19 właściwości w swojej klasie i w większości przypadków tylko 2 lub 3 będą musiały być oglądane. Dlatego rozważam wdrożenie każdej właściwości za pomocą Lazy<T>. Jednak, aby utworzyć każdą właściwość, wykonuję interpolację liniową (lub interpolację dwuliniową), która jest dość trywialna, ale wiąże się z pewnymi kosztami. (Czy zamierzasz zasugerować, żebym wybrał się na własny eksperyment?)
Ben
3
James, korzystając z moich rad, zrobiłem własny eksperyment. Zobacz mój post .
Ben
17
Możesz zainicjować / zainicjować wszystko „podczas” uruchamiania systemu, aby zapobiec opóźnieniom użytkownika w systemach o wysokiej przepustowości i niskim opóźnieniu. To tylko jeden z wielu powodów, dla których „nie zawsze” używam Lazy.
Derrick
126

Powinieneś starać się unikać korzystania z Singletonów, ale jeśli kiedykolwiek zajdzie taka potrzeba, Lazy<T>ułatwia wdrażanie leniwych, bezpiecznych dla wątków singletonów:

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;
}
Mateusz
źródło
38
Nienawidzę czytać Powinieneś starać się unikać korzystania z Singletonów, kiedy ich używam: D ... teraz muszę się dowiedzieć, dlaczego powinienem ich unikać: D
Bart Calixto
24
Przestanę używać Singletonów, gdy Microsoft przestanie ich używać w swoich przykładach.
eaglei22
4
Zwykle nie zgadzam się z opinią, że muszę unikać Singletonów. Przestrzeganie paradygmatu wstrzykiwania zależności nie powinno mieć znaczenia. Idealnie byłoby, gdyby wszystkie zależności były tworzone tylko raz. Zmniejsza to nacisk na GC w scenariuszach dużego obciążenia. Dlatego uczynienie ich Singletonami z samej klasy jest w porządku. Większość (jeśli nie wszystkie) nowoczesne kontenery DI mogą sobie z tym poradzić w dowolny sposób.
Lee Grissom,
1
Nie musisz używać takiego wzorca singletonu, zamiast tego użyj dowolnego kontenera di skonfiguruj swoją klasę dla singletonu. Pojemnik zajmie się dla ciebie kosztami ogólnymi.
VivekDev,
Wszystko ma swój cel, zdarzają się sytuacje, w których singletony są dobrym podejściem i sytuacje, w których tak nie jest :).
Hawkzey
86

Wielki świecie rzeczywistym przykładem gdzie leniwe ładowanie przydaje się z ORM (Object za Relacja odwzorowujące), takich jak Entity Framework i NHibernate.

Załóżmy, że masz klienta będącego klientem, który ma właściwości dotyczące nazwy, numeru telefonu i zamówień. Nazwa i numer telefonu są ciągami ciągłymi, ale Zamówienia to właściwość nawigacji, która zwraca listę wszystkich zamówień, które klient kiedykolwiek złożył.

Często możesz przejrzeć listę wszystkich klientów i uzyskać ich nazwę i numer telefonu, aby do nich zadzwonić. Jest to bardzo szybkie i proste zadanie, ale wyobraź sobie, że za każdym razem, gdy tworzysz klienta, automatycznie przechodzi on i wykonuje złożone połączenie, aby zwrócić tysiące zamówień. Najgorsze jest to, że nawet nie zamierzasz korzystać z zamówień, więc jest to marnowanie zasobów!

Jest to idealne miejsce do leniwego ładowania, ponieważ jeśli właściwość Order jest leniwa, nie pobierze całego zamówienia klienta, chyba że faktycznie ich potrzebujesz. Możesz wyliczyć obiekty klienta, otrzymując tylko ich nazwę i numer telefonu, gdy właściwość Zamówienie cierpliwie śpi i jest gotowa, gdy będziesz tego potrzebować.

Despertar
źródło
34
Zły przykład, ponieważ takie leniwe ładowanie zwykle jest już wbudowane w ORM. Nie powinieneś zaczynać dodawać Lazy <T> do swoich POCO, aby uzyskać leniwe ładowanie, ale skorzystaj z metody specyficznej dla ORM.
Dynalon
56
@Dyna Ten przykład odnosi się do wbudowanego leniwego ładowania ORM, ponieważ myślę, że ilustruje to przydatność leniwego ładowania w jasny i prosty sposób.
Despertar
Więc jeśli korzystasz z Entity Framework, czy należy egzekwować własne lenistwo? A może EF robi to za Ciebie?
Zapnologica
7
@Zapnologica EF robi to wszystko domyślnie. W rzeczywistości, jeśli chcesz chętnego ładowania (w przeciwieństwie do leniwego ładowania), musisz jawnie powiedzieć EF, używając Db.Customers.Include("Orders"). Spowoduje to, że łączenie zamówienia zostanie wykonane w tym momencie, a nie przy Customer.Orderspierwszym użyciu właściwości. Leniwe ładowanie można również wyłączyć za pomocą DbContext.
Despertar
2
W rzeczywistości jest to dobry przykład, ponieważ można chcieć dodać tę funkcjonalność podczas korzystania z czegoś takiego jak Dapper.
tbone
41

Zastanawiałem się nad użyciem Lazy<T>właściwości do poprawy wydajności własnego kodu (i aby dowiedzieć się więcej na ten temat). Przyszedłem tutaj, szukając odpowiedzi na temat tego, kiedy go użyć, ale wygląda na to, że wszędzie, gdzie idę, są takie frazy:

Użyj opóźnionej inicjalizacji, aby odroczyć utworzenie dużego lub zasobochłonnego obiektu, lub wykonanie zadania wymagającego dużej ilości zasobów, zwłaszcza gdy takie tworzenie lub wykonanie może nie nastąpić w trakcie życia programu.

z MSDN Lazy <T> klasy

Jestem trochę zdezorientowany, ponieważ nie jestem pewien, gdzie narysować linię. Na przykład interpolację liniową traktuję jako dość szybkie obliczenie, ale jeśli nie muszę tego robić, to czy leniwa inicjalizacja pomoże mi tego uniknąć i czy warto?

W końcu postanowiłem wypróbować własny test i pomyślałem, że podzielę się tutaj wynikami. Niestety tak naprawdę nie jestem ekspertem w przeprowadzaniu tego rodzaju testów, dlatego cieszę się z komentarzy, które sugerują ulepszenia.

Opis

W moim przypadku szczególnie zainteresowałem się tym, czy Lazy Properties może pomóc w ulepszeniu części mojego kodu, która wykonuje wiele interpolacji (większość jest nieużywana), dlatego stworzyłem test porównujący 3 podejścia.

Stworzyłem osobną klasę testową z 20 właściwościami testowymi (nazwijmy je właściwościami t) dla każdego podejścia.

  • Klasa GetInterp: Wykonuje interpolację liniową za każdym razem, gdy otrzymywana jest właściwość t.
  • InitInterp Class: Inicjuje właściwości t, uruchamiając interpolację liniową dla każdej z konstruktorów. Get po prostu zwraca podwójne.
  • InitLazy Class: Ustawia właściwości t jako właściwości Lazy, dzięki czemu interpolacja liniowa jest uruchamiana raz, gdy właściwość jest pobierana po raz pierwszy. Kolejne pobrania powinny po prostu zwrócić już obliczone podwójne.

Wyniki testu są mierzone w ms i są średnią z 50 instancji lub 20 otrzymanych właściwości. Każdy test był następnie przeprowadzany 5 razy.

Wyniki testu 1: Instancja (średnio 50 instancji)

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

Wyniki testu 2: Pierwsze pobranie (średnio 20 nieruchomości dostaje)

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

Wyniki testu 3: Second Get (średnio 20 nieruchomości dostaje)

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

Spostrzeżenia

GetInterpjest najszybszy do utworzenia zgodnie z oczekiwaniami, ponieważ nic nie robi. InitLazyjest szybsze do tworzenia instancji niż InitInterpsugerowanie, że narzut związany z konfigurowaniem leniwych właściwości jest szybszy niż moje obliczenia interpolacji liniowej. Jestem jednak trochę zdezorientowany, ponieważ InitInterppowinienem wykonywać 20 interpolacji liniowych (aby ustawić właściwości t), ale utworzenie instancji (test 1) zajmuje tylko 0,09 ms, podczas gdy GetInterpwykonanie tylko jednej interpolacji liniowej zajmuje 0,28 ms za pierwszym razem (test 2) i 0,1 ms, aby zrobić to za drugim razem (test 3).

Zdobycie właściwości za pierwszym razem zajmuje InitLazyprawie 2 razy GetInterp, podczas gdy InitInterpjest najszybsze, ponieważ wypełniło swoje właściwości podczas tworzenia wystąpienia. (Przynajmniej tak powinno być, ale dlaczego wynik tworzenia instancji był o wiele szybszy niż pojedyncza interpolacja liniowa? Kiedy dokładnie wykonuje te interpolacje?)

Niestety wygląda na to, że w moich testach dzieje się automatyczna optymalizacja kodu. GetInterpZdobycie właściwości za pierwszym razem powinno zająć tyle samo czasu, co za drugim razem, ale pokazuje się ponad dwukrotnie szybciej. Wygląda na to, że ta optymalizacja wpływa również na inne klasy, ponieważ wszystkie zajmują tyle samo czasu na test 3. Jednak takie optymalizacje mogą również mieć miejsce w moim własnym kodzie produkcyjnym, co również może być ważnym czynnikiem.

Wnioski

Chociaż niektóre wyniki są zgodne z oczekiwaniami, istnieją również bardzo interesujące nieoczekiwane wyniki, prawdopodobnie z powodu optymalizacji kodu. Nawet w przypadku klas, które wyglądają, jakby wykonały dużo pracy w konstruktorze, wyniki tworzenia instancji pokazują, że ich tworzenie może być bardzo szybkie, w porównaniu do uzyskania podwójnej właściwości. Podczas gdy eksperci w tej dziedzinie mogą być w stanie komentować i badać dokładniej, moim osobistym odczuciem jest to, że muszę ponownie wykonać ten test, ale na moim kodzie produkcyjnym, aby sprawdzić, jakie rodzaje optymalizacji mogą tam mieć miejsce. Spodziewam się jednak, że InitInterpmoże to być właściwa droga.

Ben
źródło
26
być może powinieneś opublikować swój kod testowy do odtwarzania wyników, ponieważ bez znajomości kodu trudno będzie cokolwiek sugerować
WiiMaxx
1
Uważam, że głównym kompromisem jest użycie pamięci (leniwe) i użycie procesora (nie leniwe). Ponieważ lazywymaga dodatkowej księgowości, InitLazyzużyłby więcej pamięci niż inne rozwiązania. Może również mieć niewielki spadek wydajności przy każdym dostępie, podczas gdy sprawdza, czy ma już wartość, czy nie; sprytne sztuczki mogłyby usunąć to obciążenie, ale wymagałoby to specjalnego wsparcia w IL. (Haskell robi to przez wywołanie każdej leniwej wartości wywołaniem funkcji; po wygenerowaniu wartości jest ona zastępowana funkcją, która zwraca tę wartość za każdym razem.)
jpaugh
14

Wystarczy wskazać na przykład opublikowany przez 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; }
    }
}

zanim urodziliby się Lazy, zrobilibyśmy to w ten sposób:

private static object lockingObject = new object();
public static LazySample InstanceCreation()
{
    if(lazilyInitObject == null)
    {
         lock (lockingObject)
         {
              if(lazilyInitObject == null)
              {
                   lazilyInitObject = new LazySample ();
              }
         }
    }
    return lazilyInitObject ;
}
Thulani Chivandikwa
źródło
6
Zawsze używam do tego kontenera IoC.
Jowen
1
Zdecydowanie zgadzam się na rozważenie w tym celu kontenera IoC. Jeśli jednak chcesz mieć prosty, leniwy, zainicjowany singleton obiektu, zastanów się również, że jeśli nie potrzebujesz tego, aby być bezpiecznym dla wątków, rób to ręcznie za pomocą parametru If, który może być najlepszy, biorąc pod uwagę narzut związany z wydajnością samego Lazy.
Thulani Chivandikwa
12

Z MSDN:

Użyj instancji Lazy, aby odroczyć utworzenie dużego lub wymagającego dużych zasobów obiektu lub wykonanie zadania wymagającego dużych zasobów, szczególnie gdy takie tworzenie lub wykonanie może nie nastąpić w trakcie życia programu.

Oprócz odpowiedzi Jamesa Michaela Hare'a, Lazy zapewnia bezpieczną dla wątków inicjalizację twojej wartości. Spójrz na pozycję MSDN wyliczenia LazyThreadSafetyMode opisującą różne typy trybów bezpieczeństwa wątków dla tej klasy.

Wazon
źródło
-2

Powinieneś spojrzeć na ten przykład, aby zrozumieć architekturę Lazy Loading

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");
    }
}

-> wyjście -> 0 1 2

ale jeśli ten kod nie pisze „list.Value.Add (0);”

wynik -> Wartość nie została utworzona

Kaczka Tufy
źródło