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:
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:
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.