Klasa Lazy w .NET 4.0

Przez przypadek dokładając swoją cegiełkę do dyskusji o tym, czy wzorzec singielton jest dobry czy zły, doszukałem się dość ciekawej klasy w .NET 4.0, mianowicie klasy Lazy<T> wprowadzonej w .NET 4.0. Klasa ma dość rozbudowany jak na C# sposób obsługi tworzenia obiektów z opóźnioną inicjalizacją w aplikach wielowątkowych. Poniżej, zwięzły opis i przykład zastosowania.

W sumie dość obszerny opis tej klasy można znaleźć w dokumentacji msdn: https://msdn.microsoft.com/pl-pl/library/dd642331%28v=vs.110%29.aspx, poniżej jednak prezentuje bardziej zwięzły i jasny opis podstaw.

Lazy<T>

Nowością wprowadzoną w .NET 4.0 jest klasa Lazy<T>. Służy ona do opóźniania inicjalizacjii obiektu(najprościej to sobie wyobrazić, że chcemy opóźnić tworzenie obiektu, ponieważ jest bardzo duży i zasobożerny i nie mamy pewności czy w ogóle będzie potrzebny w trakcie działania programu).
Konstruktor tej klasy przyjmuje delegata(jedna z możliwości), w którym następuje utworzenie żądanego obiektu. Dopóki nie będziemy potrzebować obiektu T, klasa Lazy<T> nie będzie tworzyć tego obiektu. W momencie pierwszego użycia obiekt zostanie stworzony.

Dodatkowo klasa zapewnia poprawną obsługę w aplikacjach wielowątkowych – to znaczy gwarantuje, że samo utworzenie obiektu T na żądanie będzie bezpieczne. Jednak nie gwarantuje, że sam obiekt T będzie bezpieczny w środowisku wielowątkowym (co jest oczywiste). Ale o tym później.

Najprostsze utworzenie klasy Lazy; wygląda następująco:

var lazyInit = new Lazy<T>();

// co jest równoważne z poniższym zapisem

var lazyInit = new Lazy<T>(()=> new T(), LazyThreadSafetyMode.None);

Argumentem konstruktora może być delegat(Func<T>), który zwraca typ T. Dzięki temu, że to delegat możemy stworzyć bardziej złożoną inicjalizację obiektu T(taki delegat można potraktować jako implementacja wzorca fabryka metod). Kolejnym argumentem jest flaga decydująca o zachowaniu się obiektu w przypadku aplikacji wielowatkowych.

Klasa udostępnia dwie właściwości.

IsValueCreated sprawdza czy wartość jest już stworzona oraz Value, w której jest przechowywany obiekt T.

Wielowątkowość

I tu dochodzimy do tego co najciekawsze w klasie Lazy<T> czyli obsługi wielu wątków. Poniżej omówienie przykładu ze środowiskiem wielowątkowym.

ExecutionAndPublication

Klasę Lazy<T> może tak stworzyć, że tylko jeden wątek będzie tworzył obiekt a reszta wątków już będzie korzystać z tego utworzonego obiektu(parametr LazyThreadSafetyMode.ExecutionAndPublication). Nie będzie wtedy występował problem wyścigu wątków.

PublicationOnly

Kolejnym przypadkiem jest wywołanie konstruktora klasy Lazy<T> tak, aby każdy wątek mógł tworzyć obiekty ale tylko jedna instancje stworzonego obiektu będzie wykorzystywana przez wszystkie wątki (LazyThreadSafetyMode.PublicationOnly)

None

Może również zdefiniować, że tworzenie obiektu będzie odbywało się w każdym wątku i wyłączyć obsługę środowiska wielowątkowego(LazyThreadSafetyMode.None). Jednak wtedy tracimy wszystkie zalety klasy Lazy<T> i jej obsługi wielu wątków. Parametr ten służy do wywoływania w aplikacjach jedno wątkowych.

Przykład

Poniżej zamieszam przykład, który będzie obejmował opóźnione wywołanie obiektu przy użyciu wielu wątków wraz z pokazaniem różnic w tworzeniu klasy Lazy<T>.

Wyobraźmy sobie, że posiadamy duży obiekt, którego inicjalizacje chcemy opóźnić.

public class LazyObject
{
public int ThreadNumber { get; private set; }

public LazyObject()
{
ThreadNumber = Thread.CurrentThread.ManagedThreadId;

Console.WriteLine("Utworzenie obiektu w wątku: {0}", ThreadNumber);
}

~LazyObject()
{
Console.WriteLine("GC: Usunięcie obiektu stworzonego w wątku {0}", ThreadNumber);
}
}

Obiekt ten posada konstruktor, który będzie informował komunikatem na konsoli, w którym wątku będzie stworzony. Destruktor służy pokazaniu, kiedy obiekt zostanie usunięty przez GC (Co należy wiedzieć o GC?)

Następnie tworzymy 3 wątkowy program, który będzie drukował na konsoli numer wątku w którym został stworzony obiekt LazyObject. Robimy tak po to by zobaczyć, który wątek stworzył obiekt i móc rozróżniać pojedyncze tworzenie obiektu.

public class LazyExample
{
public LazyExample()
{
var lazyObject = new Lazy<LazyObject>(() => new LazyObject(), LazyThreadSafetyMode.ExecutionAndPublication);

Action taskMethod = () => Console.WriteLine("Akcja na obiekcie stworzonym w wątku : {0}", lazyObject.Value.ThreadNumber);

var taskArray = new Task[3];

for (int i = 0; i < 3; i++)
{
taskArray[i] = new Task(taskMethod);
taskArray[i].Start();
}

foreach (var task in taskArray)
{
task.Wait();
}

GC.Collect();
Thread.Sleep(1000);
}
}

Warto zwrócić uwagę na parametr LazyThreadSafetyMode.ExecutionAndPublication. Oznacza to, że tylko jeden obiekt zostanie stworzony a każdy wątek powinien działać na tym samym obiekcie LazyObject. Po uruchomieniu kodu:

wynik z konsoli potwierdza założenia:

lazy_2
Teraz zmienimy parametr LazyThreadSafetyMode na PublicationOnly. Efektem powinno być stworzenie 3 obiektów. Wykonanie akcji powinno odbywać się tylko na jeden obiekcie. Powinno również nastąpić usunięcie obiektów nie używanych (czyli tych stworzonych w innych wątkach ale nigdy nie zwróconych żadnemu wątkowi) na życzenie GC ze względów, że obiekty te nie są nigdzie używane. Po uruchomieniu programu:

wynik z konsoli znów potwierdza przypuszczenia:

lazy_1

Każdy wątek pracował na obiekcie stworzonym w wątku 3. Po uruchomieniu GC widać wyraźnie, że obiekty utworzone w innych wątkach nie zostały nigdzie zwrócone.