Debugowanie aplikacji wielowątkowych: Parralel Stacks

W ostatnim wpisie pokazałem jak debugować kod .NET Framework w VS2008. Wciąż próbuje wyjaśnić w jakich wersjach to działa a w jakich nie. W następnym wpisie mam nadzieję, że wspomnę jak sprawa wygląda z VS 2010 i VS2012.

Wątki w aplikacjach są dziś już nieodłącznym elementem. Wprowadzenie async\await jeszcze bardziej upowszechniło i ułatwiło pisanie kodu wykonywanego równolegle. W przypadku, gdy aplikacja wielowątkowa zachowuje się podejrzanie, znalezienie błędu może być bardzo trudnym wyzwaniem. Visual Studio ułatwia debugowanie poprzez dostarczenie kilku mechanizmów. Najpopularniejszy to okno Threads, zawierające listę wszystkich wątków. Załóżmy, że mamy kod, w którym jeden z wątków blokuje dostęp do zasobów współdzielonych:

 

class Program
{
   private readonly static object _Sync=new object();

   static void Main(string[] args)
   {

       for (int i = 0; i < 50; i++)
       {
           Task.Factory.StartNew(() =>
           {
               lock (_Sync)
               {
                   Thread.Sleep(5000000);
               }
           });
       }
       Console.ReadLine();
   }
}

Pierwszym, najpopularniejszym ułatwieniem w VS jest okno Threads, zawierające listę wykonywanych wątków:

image

Jest chyba one bardzo dobrze znane więc nie będę za wiele tutaj pisał. W poście chciałem jednak pokazać bardziej interesujący mechanizm, a mianowicie Parralel Stacks, które można otworzyć z Debug->Windows:

image

W oknie widać dokładnie jakie wątki zostały utworzone, kiedy i w jakiej są relacji do siebie. Można zaobserwować również, że jeden z 50 utworzonych wątków, wywołał funkcje Thread.Sleep. W taki sposób, nie trudno domyślić się, dlaczego pozostałe wątki nie zakończyły zadania.

Mechanizm naprawdę pomocny szczególnie w aplikacjach co mają >100 wątków. Dzięki temu, łatwo zaobserwować gdzie i kiedy wątki są tworzone.

Debuggowanie .NET Framework

Czasami nasz kod nie działa i chcielibyśmy sprawdzić jak działa dokładnie funkcja dostarczona przez .NET Framework. Oczywiście domyślnie nie możemy tego zrobić – po kliknięciu Step Into, przejdziemy po prostu do kolejnej metody.

Poniższy opis dotyczy Visual Studio 2008, na którym testowałem i faktycznie działa to. Niestety wciąż mamy problemy z VS 2012 – w następnym wpisie postaram się dać jakiś update odnośnie 2012.

W VS mamy coś takiego jak source server oraz możemy również ściągnąć symbols files dla każdej biblioteki MS z publicznie dostępnego serwera. Przejdźmy więc do Tools->Options->Debugging->Generals i zaznaczmy :

1. Enable Source Server Support
2. Enable .NET Framework Source Code Stepping

image

Następnie przechodzimy do zakładki Symbols i zaznaczamy Microsoft Symbol Server:image

Teraz możemy przetestować debugging. Napiszmy jakiś kod wywołujący funkcje z .NET Framework np.:

internal class Program
{
   private static void Main(string[] args)
   {
       DateTime.Parse("text");
   }
}

Przekonamy się, że po prostu będziemy w stanie przejść do ciała tej funkcji.

Klasa Gemini w C#

Przed przeczytaniem tego postu zachęcam do zapoznania się następującymi wpisami:

1. Typ dynamic w C# 4.0
2. Zastosowanie dynamic: ExpandoObject
3. Zastosowanie dynamic: DynamicObject

ExpandoObject to przydatna czasami klasa, ale problem z nią taki, że nie można jej rozszerzać i nawet została oznaczona jako sealed.

Aby skorzystać z klasy Gemini, najpierw należy zainstalować odpowiedni pakiet z NuGet:

image

Zacznijmy od przykładu pokazującego, że faktycznie można po Gemini dziedziczyć:

internal class Program
{
   private static void Main(string[] args)
   {
       dynamic person=new Person();
       person.FirstName = "Piotr";

       person.DisplayMessage();
   }
}

internal class Person : Gemini
{
   public void  DisplayMessage()
   {
       Console.WriteLine(_.FirstName);
   }
}

Proszę zwrócić uwagę na wskaźnik _, który jest dynamic i dlatego może odnosić się do jakichkolwiek pól. Gdybyśmy chcieli użyć this, program nie skompilowałby się, ponieważ właściwość FirstName nie jest jawnie zadeklarowana. Wewnętrznie, Gemini implementuje po prostu DynamicObject.

Na razie jeszcze nie pokazałem żadnych nadzwyczajnych możliwości Gemini. Ciekawą jednak możliwością jest definiowanie metod globalnie, w jednym miejscu, np. w bootstrapperze aplikacji:

internal class Program
{
   private static void Main(string[] args)
   {
       Gemini.Extend<Person>(p =>
       {
           p.DisplayMessage = new DynamicMethodWithParam(text => Console.WriteLine(text + p.FirstName));
       });


       dynamic person = new Person();
       person.FirstName = "Piotr";

       person.DisplayMessage(); // wyswietla "Piotr"
       person.DisplayMessage("Witaj "); // wyswietla "Witaj Piotr"
   }
}

internal class Person : Gemini
{
   public void  DisplayMessage()
   {
       Console.WriteLine(_.FirstName);
   }
}

Analogicznie można zastąpić już istniejące metody. W powyższym przypadku rozszerzamy funkcjonalność o dodatkową metodę, przyjmującą parametr wejściowy. Możliwe jest również rozszerzenie konkretnej specyfikacji:

 p.DisplayMessage = new DynamicMethodWithParam(text => Console.WriteLine(text + p.FirstName));

To chyba akurat nie jest szczególnie interesujące bo podobną funkcjonalność dostarczał ExpandoObject. Gdy próbujemy wywołać metodę, która nie istnieje wtedy zostanie wywołana MethodMissing:

internal class Person : Gemini
{
   public void DisplayMessage()
   {
       Console.WriteLine(_.FirstName);
   }

   private dynamic MethodMissing(dynamic callInfo)
   {
       Console.WriteLine(callInfo.Name);
       throw new NotImplementedException();
   }
}

Obiekt callInfo zawiera informacje o metodzie, którą użytkownik chciał wywołać. Możemy wyrzucić wyjątek (jak w przypadku powyżej) lub stworzyć implementacje i ją wstrzyknąć tzn.:

dynamic MethodMissing(dynamic callInfo)
{
  var member = callInfo.Name.Replace("Html", "");
  SetMember(callInfo.Name,
      new DynamicFunction(() =>
      {
          return "Wstrzyknieta metoda zwracajaca string.";
      });

  return GetMember(callInfo.Name)();
}

Powyższa konstrukcja to tak naprawdę lazy loading – definiujemy metodę, dopiero gdy faktycznie jest ona potrzebna.

W następnym poście postaram się pokazać jakiś konkretny, praktyczny przykład.

Code review: statyczne pole w generycznej klasie

Dzisiaj króciutki przykład:

class ItemInfo<T>
{
    public string Name{get;set;}
    public T Value{get;set;}
    
    private static int AnyField;
}

Powyższy kod nie zawsze jest złym wzorcem ale bardzo często może nim być. Należy sobie zdać sprawę, że statyczne pole nie będzie tutaj tworzone dla każdego obiektu ItemInfo. Będzie one współdzielone wyłącznie przez takie same typy. Przykład ilustrujący działanie:

internal class Program
{
   private static void Main(string[] args)
   {
     var integerInfo=new ItemInfo<int>();
     var stringInfo = new ItemInfo<string>();
     var integerInfo2 = new ItemInfo<int>();

    integerInfo.UpdateField(10);
    stringInfo.UpdateField(20);
    integerInfo2.UpdateField(30);
    
    Console.WriteLine(integerInfo.GetStaticField()); //30
    Console.WriteLine(stringInfo.GetStaticField()); // 20!!!
    Console.WriteLine(integerInfo2.GetStaticField()); //30
   }
}

Sam ostatnio miałem podobny problem i po zastanowieniu się chwilkę, stwierdziłem, że faktycznie statyczne pole nie  jest potrzebne. Nie ma nic złego w statycznych polach, jeśli chcemy mieć osobny egzemplarz na każdą kombinację argumentów. To może być szczególnie nietypowy scenariusz, gdy mamy wiele parametrów:

class ItemInfo<T1,T2,T3,T4,T5>
{
}

Łatwo sobie wyobrazić ile kombinacji użytkownik może przekazać…

WPF: Border, CornerRadius oraz przycinanie zawartości

Ostatnio potrzebowałem użyć Border z CornerRadius ustawionym na jakąś wartość, aby móc potem umieścić w środku inne kontrolki (np. obrazek). Moje pierwsze podejście było następujące:

<Border CornerRadius="50" BorderBrush="Black" BorderThickness="2">
  <Image Stretch="Fill" Source="http://i.zdnet.com/blogs/win7-wallpaper-small.png"></Image>
</Border>

Niestety w taki sposób zawartość Border nie zostanie poprawnie przycięta i efekt jest następujący:

image

Znalazłem na forum bardzo ciekawe rozwiązanie z użyciem OpacityMask. Najpierw kod a potem wyjaśnienie:

<Border CornerRadius="50" BorderBrush="Black" BorderThickness="2">
  <Grid>
      <Border CornerRadius="50" Background="White" BorderThickness="2" x:Name="mask"/>
      
      <Image Stretch="Fill" Source="http://i.zdnet.com/blogs/win7-wallpaper-small.png">
              <Image.OpacityMask>
                  <VisualBrush Visual="{Binding ElementName=mask}"></VisualBrush>
              </Image.OpacityMask>
          </Image>
  </Grid>
</Border>

Efekt:

image

Aby zrozumieć powyższy kod należy najpierw wyjaśnić jak działa OpacityMask. OpacityMask to brush, który określa, co na docelowym obrazku będzie wyświetlone. Myślę, że poniższy obrazek z MSDN jest doskonałym wyjaśnieniem:

Object with a LinearGradientBrush opacity mask

Innymi słowy, OpacityMask służy do ustawiania przezroczystości poszczególnych pikseli.

W naszym przypadku korzystamy z VisualBrush, który wygeneruje pędzel na podstawie innej kontrolki. Stworzyliśmy drugą kontrolkę border jako maskę. Posiada ona ten sam CornerRadius, co oznacza, że po wygenerowaniu pędzla, rogi kontrolki będą przezroczyste! I o to właśnie chodzi – po nałożeniu tej maski na obrazek, przytniemy odpowiednio narożniki. To co było przezroczyste w kontrolce “mask” (narożniki) będzie również niewidzialne na docelowym elemencie (obrazku).

Singleton oraz WeakReference

W zdecydowanej większości przypadków jestem przeciwnikiem singleton’a i uważam to za anty-wzorzec. Dużo lepiej użyć IoC i przekazywać wszędzie w konstruktorach tą samą instancję. Istnieją jednak przypadki, w których użycie singleton’a nie jest brzydkie.

Ostatnio miałem klasę, która potrzebowała pewnych danych – kolekcję prostych struktur. Każda struktura zawiera string i pole bool. Ze względu, że ta kolekcja musi być wykorzystana w kilku klasach, zdecydowałem się na przeniesienie jej do osobnej klasy (singleton’a), która tworzy ją na żądanie.

Załóżmy, że pojedyncza struktura wygląda następująco:

struct ItemInfo
{
   public readonly string Name;

   public ItemInfo(string name)
   {
       Name = name;
   }
}

Następnie w kilku klasach potrzebujemy  kolekcji ItemInfo, zawierającej różne nazwy. Singleton nie jest najgorszym rozwiązaniem i mógłby wyglądać następująco:

class DataProvider
{
   private DataProvider()
   {
   }

   private static DataProvider _dataProvider;

   public static DataProvider Instance
   {
       get { return _dataProvider ?? (_dataProvider = new DataProvider()); }
   }

   private ItemInfo[] _data;
   public ItemInfo[] GetData()
   {
       if (_data == null)
       {
           _data = new[] {new ItemInfo("a"), new ItemInfo("b"), new ItemInfo("c")};
       }
       return _data;
   }
}

Cel został osiągnięty  – teraz wszystkie klasy mogą korzystać ze współdzielonych danych. Nie musimy tworzyć kilka razy ItemInfo zawierających te SAME dane.

Niestety, takie dane będą w pamięci przez cały czas życia aplikacji, co nie zawsze ma sens. Przecież możliwe jest, że potrzebujemy ich tylko w danym, specyficznym kontekście. Dobrze byłoby jakoś pozbyć się danych z pamięci, ale jednocześnie nie tracić wspomnianych korzyści oraz nie pisać zbyt wiele kodu.

W tym momencie możemy skorzystać z WeakReference, a mianowicie:

class DataProvider
{
   private DataProvider()
   {
   }

   private static WeakReference<DataProvider> _dataProviderReference;

   public static DataProvider Instance
   {
       get
       {
           DataProvider dataProvider;
    
           if (_dataProviderReference == null)
           {
               dataProvider = new DataProvider();
               _dataProviderReference = new WeakReference<DataProvider>(dataProvider);
           }
           else if (!_dataProviderReference.TryGetTarget(out dataProvider))
           {
               dataProvider = new DataProvider();
               _dataProviderReference.SetTarget(dataProvider);
           }

           return dataProvider;
       }
   }

   private ItemInfo[] _data;
   public ItemInfo[] GetData()
   {
       if (_data == null)
       {
           _data = new[] {new ItemInfo("a"), new ItemInfo("b"), new ItemInfo("c")};
       }
       return _data;
   }
}

Dzięki takiemu rozwiązaniu, gdy nikt nie korzysta z provider’a, jego instancja po prostu zostanie zwolniona. Trzeba być jednak bardzo uważnym, aby nie skończyło się na tym, że GC zbiera co chwilę zasoby, a my wciąż wywołujemy Instance, tworząc za każdym razem nowy obiekt.

Nazwa Provider może nie jest trafna, ponieważ zwykle implementacja takich klas w formie singleton’a jest złym rozwiązaniem. W moim, konkretnych przykładzie, wszystko było wykonywane w pamięci  – proste kontenery zawierające listę różnych współczynników.

Code Review: IoC oraz zbyt wiele parametrów w konstruktorze

Dzisiejszy wpis dotyczy wszystkich klas, jednak zostanie on zaprezentowany na przykładzie ViewModel znanego z MVVM. W moim projekcie używam MVVM i dlatego jest to dla mnie naturalne. W poście zaprezentuje bardzo sztuczne przykłady, dlatego proszę nie skupiać się na nazewnictwie czy na zaprezentowanej funkcjonalności. Przedstawiony problem jest jednak bardzo częsty w realnych projektach i sam mam\miałem  z nim do czynienia w codziennej pracy.

Powiedzmy, że napisaliśmy ViewModel prezentujący jakąś część logiki w naszej aplikacji:

class EmployeeViewModel
{
    public void PrintReport()
    {
        EmployeeValidator validator=new EmployeeValidator();
        
        if(validator.IsValid(Employee))
        {
            DataManager dataManager = new DataManager();
            dataManager.Save(Employee);
            
            ReportPrinter reportPrinter = new ReportPrinter();
            reportPrinter.Print(Employee);
        }
    
    }
}

Powyższy kod ma wiele wad. Zacznijmy jednak od zalet. Dobrą praktyką jest, że logika nie została umieszczona bezpośrednio w ViewModel a została oddelegowana do osobnych klas typu ReportPrinter. ViewModel nie powinien stanowić implementacji wszystkich dostępnych funkcjonalności na danej formatce.

Przejdźmy do wad – powyższego kodu nie da się przetestować. Aby to umożliwić możemy spróbować wstrzyknąć implementacje w konstruktorze:

class EmployeeViewModel
{
    private readonly IEmployeeValidator _validator;
    private readonly IDataManager _dataManager;
    private readonly IReportPrinter _reportPrinter;
    
    public EmployeeViewModel(IEmployeeValidator validator, IDataManager dataManager, IReportPrinter reportPrinter)
    {
        _validator = validator;
        _dataManager = dataManager;
        _reportPrinter = reportPrinter;
    }
    
    public void PrintReport()
    {
        if(_validator.IsValid(Employee))
        {
            _dataManager.Save(Employee);
            _reportPrinter.Print(Employee);
        }
    }
}

Dużo lepiej ponieważ programujemy teraz z użyciem interfejsów a nie konkretnych implementacji. Jeśli chcemy mieć większą kontrolę kiedy dokładnie klasy są tworzone możemy skorzystać z wzorca factory:

class EmployeeViewModel
{
    private readonly Func<IEmployeeValidator> _validatorFactory;
    private readonly Func<IDataManager> _dataManagerFactory;
    private readonly Func<IReportPrinter> _reportPrinterFactory;
    
    public EmployeeViewModel(Func<IEmployeeValidator> validatorFactory, Func<IDataManager> dataManagerFactory, Func<IReportPrinter> reportPrinterFactory)
    {
        _validatorFactory = validatorFactory;
        _dataManagerFactory = dataManagerFactory;
        _reportPrinterFactory = reportPrinterFactory;
    }
    
    public void PrintReport()
    {
        IEmployeeValidator validator = _validatorFactory();
        
        if(validator.IsValid(Employee))
        {
            IDataManager dataManager=_dataManagerFactory();
            dataManager.Save(Employee);
            
            IReportPrinter printer = _reportPrinterFactory();
            printer.Print(Employee);
        }
    }
}

Bardzo często, szczególnie jednak w przypadku ViewModel, zachodzi potrzeba wykonywania różnych operacji. Po jakimś czasie, konstruktor takiego ViewModel, mógłby wyglądać następująco:

public EmployeeViewModel(IEmployeeValidator validator, IDataManager dataManager, IReportPrinter reportPrinter, IWebService webService,IService1 service2,IService2, IService3 service3 )
{
    // proste przypisania tutaj...
}

Za bardzo nie mam pomysłów na wymyślenie przykładu więc po prostu użyłem nazw IService1, IService2 itd. Bardzo często używając IoC (szczególnie w połączeniu z ViewModel), kończymy ze skomplikowanymi konstruktorami ponieważ każdą klasę jako chcemy utworzyć wewnątrz obiektu, musimy przekazać przez konstruktor.

Zastanówmy się, co jest przyczyną tego. Ktoś mógłby powiedzieć, że jest to po prostu natura IoC i każda implementacja musi być przekazywana w tym samym miejscu (konstruktor).

Moim zdaniem jest to ogromna zaleta IoC, że łamanie zasady pojedynczej odpowiedzialności jest tak dobitnie pokazane. Gdybyśmy nie korzystali z IoC, inicjalizacje klas byłyby ukryte wewnątrz metod – na pierwszy rzut oka klasa wyglądałaby w porządku. Dzięki IoC, otrzymujemy konstruktor z np. 10 parametrami i wiemy, że klasa łamie zasadę pojedynczej odpowiedzialności i musi być zrefaktoryzowana.

Jeśli jakaś metoda lub konstruktor mają 5 lub więcej parametrów, wtedy zdecydowanie muszą być zrefaktoryzowane. Idealnie jak mają do 3 parametrów wejściowych i tylko jeden wyjściowy. Jak poradzić sobie zatem z konstruktorami w przypadku IoC? Oczywiście musimy dążyć do utrzymania pojedynczej odpowiedzialności poprzez stworzenie nowych klas.

Wystarczy tak naprawdę pogrupować parametry w logiczne, spójne komponenty. Jeśli do wydrukowania raportu potrzebujemy 3 innych klas, wtedy możemy stworzyć nową usługę:

class EmployeeReportProcessor: IEmployeeReportProcessor
{
    private readonly IEmployeeValidator _validator;
    private readonly IDataManager _dataManager;
    private readonly IReportPrinter _reportPrinter;
    
    public EmployeeReportProcessor(IEmployeeValidator validator, IDataManager dataManager, IReportPrinter reportPrinter)
    {
        _validator = validator;
        _dataManager = dataManager;
        _reportPrinter = reportPrinter;
    }
    
    public void Print(Employee employee)
    {
        if(_validator.IsValid(employee))
        {
            _dataManager.Save(employee);
            _reportPrinter.Print(employee);
        }
    }

}

Trzy parametry to idealna sytuacja. EmployeeProcessor nie będzie miał więcej odpowiedzialności i dotyczy tylko jednej operacji, jaką może wykonać użytkownik. Następnie możemy zaktualizować ViewModel:

public EmployeeViewModel(IEmployeeReportProcessor employeeReportProcessor, IWebService webService,IService1 service2,IService2, IService3 service3 )
{
    // proste przypisania tutaj...
}

Po prostej operacji agregacji, udało się zastąpić 3 parametry jednym. To samo robimy z kolejnymi parametrami. Staramy się wyszukać grupy o podobnej funkcjonalności i dodajemy nowa klasę agregującą. Jeśli wszystkie parametry stanowią kompletnie odrębną od siebie logikę wtedy znaczy, że takowy ViewModel jest błędnie napisany i powinien zostać rozdzielony na kilka. Często zaczynamy od jednego VM na formatkę co nie zawsze jest dobrym pomysłem. Czasami widok, musi być rozdzielony na kilka VM. Powyższe rozwiązanie, dotyczące agregacji usług dotyczy wyłącznie sytuacji, gdzie faktycznie ViewModel funkcjonalnie dotyczy tego samego obszaru w aplikacji, ale ze względu na dużą liczbę różnych usług, jego konstruktor jest ogromny.

W niektórych sytuacjach, pojedyncza agregacja nie wystarczy i np. należy stworzyć kilka hierarchii agregatorów a następnie przekazać ten na samej dole do VM.

Code review: synchronizacja danych, przypisanie

Często można usłyszeć, że przypisania są zawsze bezpieczne w wielowątkowości i powinniśmy martwić się np. inkrementacją. Jest to prawda dla Int32 ale dla long już nie zawsze. Przykład:

internal class Program
{
   private static long _x = 0;
   private static void Main(string[] args)
   {
       Task.Factory.StartNew(Task1);
       Task.Factory.StartNew(Task2);
       Thread.Sleep(5000);

   }
   private static void Task2()
   {
       while (true)
       {
           Console.WriteLine(_x);
       }
   }
   private static void Task1()
   {
       while (true)
       {
           if (_x == 0)
               _x = long.MaxValue;
           else
               _x = 0;
       }
   }
}

W kodzie mamy zwykle przypisania. Spodziewalibyśmy się, że na ekranie będzie zawsze 0 albo long.MaxValue – w końcu żadnej innej wartości nie ustawiamy tutaj. Jeśli uruchomimy powyższy kod w trybie x86 otrzymamy:

image

Dlaczego na ekranie mamy tak różne wartości? Operacje na Int64  są bezpieczne wyłącznie, jeśli kod jest skompilowany i wykonany na procesor x64. Dla x86 są one rozdzielane na dwie operacje, które ustawiają najpierw 32 bitów i potem w drugiej operacji kolejne bity. Kompilując ten sam kod pod x64 dostaniemy oczekiwane rezultaty:

image

Projektując kod należy mieć to na uwadze bo np. identyfikatory często są typu long i nie wszyscy zdają sobie sprawę, że taki kod może być ryzykowny. Powyższe zachowanie nazywa się “torn reads” ponieważ czytamy tak naprawdę tylko częściowe wyniki.

Async\Await–wydajność, część IV (buforowanie)

W poprzednich wpisach omawialiśmy wewnętrzną implementację async\await. Dziś kolejne przykłady pokazujące, czego należy unikać aby optymalizować nasz kod. Jak wiemy, użycie await generuje masę kodu i niesie ze sobą alokację dodatkowych obiektów – maszyna stanów, wątki, wrappery itp.

W zasadzie sama alokacja w .NET nie jest jakiś wielkim problemem i jest znacząco szybsza niż w językach niezarządzanych. Niestety, pomimo wielu optymalizacji dokonywanych przez GC, późniejsze ich zwolnienie jest czasochłonne. Z tego względu, jeśli tylko to możliwe należy unikać zbędnej alokacji obiektów.

Rozważmy przykład. Załóżmy, że projektujemy interfejs generyczny zawierający metody typu Read, Write Calculate. Wszystkie one mają również odpowiedniki asynchroniczne. Z tego względu każdy kto chce zaimplementować później taki interfejs, musi dostarczy metodę asynchroniczną. W wielu przypadkach nie stanowi to problemu i jest czymś pożądanym. Istnieją jednak pewne sytuacje, w których nie ma najmniejszego sensu używanie wątków ponieważ operacja jest bardzo prosta i szybka. Załóżmy, że mamy taki przypadek i zaimplementowaliśmy naszą metodę następująco:

internal class Program
{
   private static void Main(string[] args)
   {
       Test();
   }

   private static async void Test()
   {
       int result = await UnoptimizedMethodAsync(5, 3);
   }

   private static async Task<int> UnoptimizedMethodAsync(int a, int b)
   {
       return a + b;
   }
}

Dobrą stroną powyższego kodu jest fakt, że operacja jest wykonywana tak naprawdę synchronicznie co ma sens dla prostych obliczeń. Kod jednak  ma pewną inną wadę.

Zaglądając do Reflector’a zobaczymy, że za każdym razem jest tworzony nowy wątek, który nie jest odpalany, a zwraca tylko wynik:

[DebuggerStepThrough, AsyncStateMachine(typeof(<UnoptimizedMethodAsync>d__3))]
private static unsafe Task<int> UnoptimizedMethodAsync(int a, int b)
{
    <UnoptimizedMethodAsync>d__3 d__;
    AsyncTaskMethodBuilder<int> builder;
    &d__.a = a;
    &d__.b = b;
    &d__.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
    &d__.<>1__state = -1;
    builder = &d__.<>t__builder;
    &builder.Start<<UnoptimizedMethodAsync>d__3>(&d__);
    return &&d__.<>t__builder.Task;
}

 

Przechodzimy więc powoli do drugiej optymalizacji. Jeśli wiemy, że metoda zwraca ten sam wynik bardzo często wtedy lepiej buforować wątek. Załóżmy, że mamy deterministyczną metodę, która z definicji powinna wspierać buforowanie ponieważ cześć parametrów wejściowych będzie powtarzać się  np. LoadModel:

private static async void Test()
{
  object model = await LoadModelAsync("model.xml");
}

private static async Task<object> LoadModelAsync(string path)
{
  return new object(); // to tylko przyklad, niewazne co zwracamy
}

Akurat w przypadku ładowania modelu, warto skorzystać z prawdziwego wątku ponieważ proces może trochę zająć. Z tego względu możemy w powyższej metodzie wywołać Task.Factory.New i wykonać operację w tle. Jeśli wywołujemy metodę kilka razy i parametr path powtarza się, wtedy warto buforować model np. za pomocą słownika. W przypadku asynchronicznej metody jednak, warto również buforować sam Task:

private static ConcurrentDictionary<string,Task<object>> _cache=new ConcurrentDictionary<string, Task<object>>();

private static async Task<object> LoadModelAsync(string path)
{
  Task<object> modelTask;

  if (_cache.TryGetValue(path, out modelTask))
      return modelTask;

  return Task.Factory.StartNew(() => new Object())
      .ContinueWith(model => _cache.TryAdd(path, model), TaskContinuationOptions.ExecuteSynchronously);
}

W powyższym kodzie buforujemy nie tylko model ale również Task. Z tego względu, gdy wywołujemy drugi raz asynchroniczną metodę nie musimy bez potrzeby alokować pamięci na Task, który i tak by w końcu nic nie robił a tylko zwracał zbuforowany model.

Podsumowując:

  1. Gdy operacja trwa bardzo krótko a interfejs wymusza zwrócenie Task (async), wykonajmy operację synchroniczne i wywołajmy np. Task.FromResult. Nie wykonujmy operacji asynchronicznie na siłę!
  2. Druga optymalizacja polega na buforowaniu Task. Często metody z natury zwracają te same wyniki (ładowanie danych z bazy itp.) W takich przypadkach programiści tworzą słownik, gdzie kluczem jest parametr metody a wartością zwracany wynik. W przypadku synchronicznych metod, takie rozwiązanie się sprawdza. Jeśli natomiast mamy metodę asynchroniczną, wtedy lepiej również buforować Task.  Jeśli mamy zbuforowany wynik, spodziewamy się, że metoda niemalże zachowa się jak inline – zwróci po prostu wynik. Bez buforowania Task, zostanie wygenerowane masę niepotrzebnego kodu, który m.in. tworzy wątek zwracający natychmiast wynik.