19/02/2009

Wycieki obiektów GDI

Home

Ostatnio zajmowałem się w pracy nieprzyjemnym problem z wyciekiem obiektów GDI. Sytuacja wydawała się dziwna ponieważ wyglądało na to, że metoda Dispose() była wołana wszędzie gdzie było to potrzebne. Po mówiąc szczerze dłuższym czasie dowiedziałem się o dwóch istotnych rzeczach:
  • Kontrolka TreeView zawiera bug polegający na tym, że jeżeli włączymy pokazywanie pól wyborów (ang. CheckBox) dla węzłów drzewa to zostaną zaalokowane cztery obiekty GDI. Nie zostaną one jednak zwolnione przy wywołaniu Dispose(). Ciekawą dyskusję na ten temat można znaleźć tutaj. Co gorsza jeśli poszperamy na sieci okaże się, że błąd jest znany od co najmniej kilku lat!
  • Drugi bug, o którym chciałem powiedzieć dotyczy klasy ImageList. Otóż klasa ta ma problemy ze zwalnianiem zasobów dla umieszczonych w niej obrazków/ikon, co doprowadza oczywiście do wzrostu liczby obiektów GDI. O błędzie tym możemy przeczytać na stronach firmy Microsft. Rozwiązanie, a raczej obejście problemu polega na wywołaniu metod GC.Collect() oraz GC.WaitForPendingFinalizers() po wywołaniu Dispose() dla instancji ImageList. Rozwiązanie działa ale korzystanie z metody GC.Collect() może mieć negatywny wpływ na wydajność aplikacji. Co gorsza może wystąpić jeszcze mniej pożądany efekt. Opieram się tutaj na doświadczeniach kolegi. Zgodnie z nimi używanie GC.Collect() w aplikacjach korzystających z kontrolek ActiveX może doprowadzić do ich zawieszenia w losowym momencie.
    ...
    imageList.Dispose();
    
    GC.Collect();
    GC.WaitForPendingFinalizers();
    ...
    
Z opisanych przeze mnie rzeczy trzeba zdawać sobie sprawę i być uważnym przy stosowania klasy TreeView, a w szczególności klasy ImageList. W przypadku posługiwania się TreeView można pokusić się jeszcze o użycie, zaproponowanej we wspomnianej przeze mnie dyskusji, klasy LeaklessTreeView (testowałem i wygląda, że działa).

17/02/2009

operator==

Home

Pomysł na napisanie tego posta podsunął mi kolega z pracy. Na początek fragment klasy, która we wzorcowy sposób dostarcza własnej implementacji operatora równości:
public class Test
{
  private int i;
  
  public Test(int i)
  {
    this.i = i;
  }

  public static bool operator==(Test a, Test b)
  {
    if (System.Object.ReferenceEquals(a, b))
      return true;

    if (((object)a == null) || ((object)b == null))
      return false;

    return a.i == b.i;
  }
  
  ...
}
Oczywiście potrzebna jest jeszcze implementacja operatora nierówności !=. Chciałbym też zwrócić uwagę, ponieważ sam zapomniałem o tym pisząc ten przykład :), że implementując operator równości (czy też nierówności) kiedy wykonujemy porównanie takie jak a == b, a == null czy b == null musimy zrzutować testowane obiekty do typu object. W innym wypadku nastąpi rekurencyjne wywołanie naszej implementacji operatora. Rzutowanie zapewni, że zostanie wywołana implementacja bazowa - operatory nie działają jak metody wirtualne. Inne rozwiązanie to użycie metody System.Object.ReferenceEquals. Teraz popatrzmy na kod poniżej i odpowiedzmy sobie na pytanie jaką wartość przyjmą zmienne lokalne res oraz res2:
public class Program
{
  public static bool MyEquals(T a, T b) where T : class
  {
    return a == b;
  }
  
  public static void Main(string[] args)
  {
    Test a = new Test(1);
    Test b = new Test(1);

    bool res = a == b;
    bool res2 = MyEquals(a, b);
  }
}
Odpowiedź brzmi oczywiście true oraz false. Piszę o tym również dla siebie ponieważ sam musiałem się zastanowić jaki będzie wynik. Pierwszy rezultat jest chyba oczywisty i nie wymaga żadnych tłumaczeń. Po prostu wywołany zostanie zaimplementowany przez nas operator. A co z wywołaniem metody generycznej MyEquals. Może się wydawać, że skoro jest to metoda generyczna to zostanie wywołany operator równości dla typu przekazanego jako argument typu generycznego (ang. generic type parameters). Argument typu generycznego to to co podstawiamy pod T czyli w tym przypadku Test.

Jest jednak inaczej. W momencie kompilacji kompilator nie wie co zostanie podstawione pod T. W związku z tym generowany jest kod sprawdzający równość referencyjną. Na poziomie IL oznacza to użycie instrukcji ceq. Jeśli zmienimy kod metody MyEquals tak jak poniżej kompilator będzie już mógł wybrać operator równości dostarczony przez nas zamiast bazowego.
public static bool MyEquals(T a, T b) where T : Test
{
  return a == b;
}

04/02/2009

Velocity (cz. 1)

Home

Velocity (z angielskiego prędkość) to mało znany projekt firmy Microsoft, którego celem jest stworzenie rozproszonej pamięci podręcznej czyli rozproszonego cache'a (nie należy mylić z projektem fundacji Apache, którego celem jest opracowanie procesora szablonów). Innymi słowy Velocity ma zwiększyć niezawodność i wydajność działania aplikacji poprzez replikację, rozproszenie oraz zapewnienie szybkiego dostępu do danych. Obecnie dostępna wersja produktu to dopiero tzw. CTP2 (ang. Community Technology Preview) czyli wersja udostępniona społecznością w celach testowych, zgłaszania uwag oraz postulatów. Nie wiadomo kiedy dokładnie pojawi się końcowy produkt ale moim zdaniem już teraz Velocity jest godne zainteresowania, a postem tym chciałbym rozpocząć krótki cykl trzech może czterech wpisów dotyczących tej technologii.

Na początek warto wymienić najważniejsze (moim zdaniem) rzeczy charakteryzujące Velocity. Zestaw ten oparłem na własnych doświadczeniach oraz na tym czego dowiedziałem się z dokumentacji i artykułów.
  • W pamięci podręcznej może zostać umieszczony każdy serializowalny obiekt.
  • Prosty interfejs programistyczny.
  • Wspiera PHP, C#, C++...
  • Możliwość wykorzystania w aplikacjach stacjonarnych, konsolowych i webowych. Przewidziana jest też integracja z dostawcą sesji ASP.NET.
  • Możliwość zarządzania i konfigurowania pomocy konsoli PowerShell.
  • Możliwość uruchomienia klastra złożonego z kilku, a nawet kilkunastu maszyn. Konfiguracja klastra może znajdować się w bazie danych lub w pliku konfiguracyjnym umieszczonym w współdzielonym katalogu.
  • Kilka typów cache'a: replikujący, partycjonujący. Typ replikujący po umieszczeniu w nim porcji danych tworzy jej kopię i umieszcza na węzłach klastra (dobre dla danych wolno-zmiennych). Typ partycjonujący rozmieszcza dane równomiernie pomiędzy poszczególne węzły klastra (dobry dla danych szybko-zmiennych). W wersji CTP2 dostępny jest tylko typ partycjonujący. Dane w cache'u partycjonującym również mogą być replikowane w celu zwiększenia bezpieczeństwa na wypadek awarii (kopia główna i poboczne).
  • Velocity wspiera silne i słaby model spójności dostępu do danych (w zależności od typu cache'a). Model spójności to umowa pomiędzy składnicą danych, a klientem mówiąca, że jeżeli klient będzie postępował zgodnie z określonymi regułami to dane nie ulegną uszkodzeniu. Słaby model spójności bazuje na jawnych zmiennych synchronizacyjnych. Silne modele spójności są implementowane przezroczyście dla klienta.
  • Operacje replikacji, partycjonowania danych, a także synchronizowania ich kopii są w pełni automatyczne i przezroczyste dla klienta.
  • Dostępne podejście optymistyczne (oparte o znaczniki, kto pierwszy ten lepszy) i pesymistyczne (wykorzystujące jawne operacje synchronizacyjne) do synchronizowania współbieżnych dostępów do danych.
  • Możliwość dynamicznego rekonfigurowania klastra poprzez dodawanie nowych węzłów.
  • Automatyczne rekonfigurowanie klastra w wypadku awarii.
  • Automatyczne równoważenie obciążenia węzłów klastra.
  • Velocity może zostać uruchomione jako usługa systemowa lub działać w procesie aplikacji.
  • Można konfigurować ilość pamięci zużywanej przez Velocity.
Osobiście pracowałem z Velocity zbyt krótko aby potwierdzić wszystkie te cechy. W szczególności testowałem tylko klaster złożony z kilku komputerów. Nie wiem też jak sprawuje się dynamiczna rekonfiguracja klastra i automatyczne równoważenie obciążenia.

W następnym poście przedstawię terminologię związaną z Velocity: gospodarz, główny gospodarz, nazwany cache itd. oraz wspomnę trochę o procesie instalowania i konfigurowania. W trzecim poście zajmę się API. Powiem również o wadach i rzeczach, które powinny moim zdaniem zostać zmienione w wersji finalnej. Dla niecierpliwych i żądnych wiedzy garść linków:

03/02/2009

Zakamarki Visual Studio (cz. 3)

Home

W trzecim już poście dotyczącym zakamarków Visual Studio, czyli mało znanych funkcji tego środowiska programistycznego, chciałbym napisać o oknie Object Test Bench. Okno to pozwala na szybkie testowanie wywołań metod statycznych i instancyjnych bez potrzeby pisania małych programików testowych. Jeśli po uruchomieniu środowiska nie jest widoczne możemy je wyświetlić wybierając View->Other Widnows->Object Test Bench. Aby wygodnie pracować z tym oknem przyda się również okno Class View (View->Class View), które służy do przeglądania typów zdefiniowanych w naszych projektach oraz w innych pakietach/podzespołach (ang. assembly), do których się odwołujemy. Ważna uwaga! Widok Object Test Bench może zostać użyty tylko i wyłącznie do testowania klas w projekcie oznaczonym jako StartUp oraz w pakietach, do których ten projekt się odwołuje. Co prawda zgodnie z dokumentacją powinniśmy móc testować wszystkie klasy w bieżącym rozwiązaniu (ang. solution) ale niestety nie jest to możliwe.

Dalej będę posługiwał się prościutką, a wręcz naiwną klasą zdefiniowaną poniżej, w celu omówienia zastosowania okna Object Test Bench.
public class Person
{
 private static int count = 0;
 
 private string name;
 private DateTime dateOfBirth;

 public string Name
 {
  get { return name; }
  set { name = value; }
 }

 public DateTime DateOfBirth
 {
  get { return dateOfBirth; }
  set { dateOfBirth = value; }
 }

 public Person()
 {
  Person.count++;
 }

 public Person(string name, DateTime dateOfBirth)
 : this()
 {
  this.name = name;
  this.dateOfBirth = dateOfBirth;
 }

 public int GetAge(bool inDays)
 {
  if(!inDays)
   return DateTime.Now.Year - dateOfBirth.Year;
  else
   return (DateTime.Now - dateOfBirth).Days ;
 }

 public static int GetCount()
 {
  return Person.count;
 }
}
Aby rozpocząć testowanie jakiejś klasy musimy zlokalizować ją w oknie Class View i wywołać dla niej menu kontekstowe. W menu kontekstowym powinny być widoczne dwie komendy:
  • Create Instance - Pozwala wywołać dowolny konstruktor dla danego typu i przypisać mu identyfikator. Stworzony obiekt pojawi się w oknie Object Test Bench. Dla stworzonych obiektów możemy następnie wywoływać metody instancyjne. W ten sposób nie powołamy do życia instancji klasy abstrakcyjnej czy też delegata.
  • Invoke Static Mehtod - Pozwala wywołać wybraną metodę statyczną danej klasy (również klasy abstrakcyjnej).
Polecenia te będą widoczne tylko dla klas z projektu głównego. Oczywiście przed wywołaniem metody statycznej, instancyjnej czy też konstruktora powinniśmy w interesujących nas miejscach w kodzie ustawić breakpointy.

Zacznijmy od wywołania metody statycznej. Po najechaniu w menu kontekstowym na polecenie Invoke Static Mehtod wyświetlone zostaną wszystkie metody statyczne dostępne w tej klasie. Należy wybrać jedną z nich. W przypadku naszej klasy testowej Person możemy wybierać pomiędzy GetCount() oraz dwie metodami odziedziczonymi po klasie Object: Equals(object, object) oraz ReferenceEquals(object, object). Po wybraniu GetCount() pojawi się okno potwierdzające chęć wywołania tej metody. Natomiast w przypadku metod, które posiadają parametry pojawi się okno pozwalające na wyspecyfikowanie wartości tych parametrów. Kiedy klikniemy Ok metoda zostanie wywołana. Na zakończenie pojawi się okno z wynikiem, które powinno wyglądać jak poniżej: Istnieje również możliwość zapisania wyniku wywołania metody. W tym celu powinniśmy zaznaczyć opcję Save return value (w oknie pokazanym powyżej) i nadać identyfikator dla zwracanego obiektu/struktury. Jeśli zdecydujemy się na zapisanie wyniku pojawi się on w oknie Object Test Bench: Stworzone obiekty reprezentowane są w oknie Object Test Bench przez prostokąty. Dla każdego obiektu możemy wywołać menu kontekstowe, które pozwoli nam wybrać metodę instancyjną do wywołania. Spróbujmy wykonać metodę posiadającą parametry np.: CompareTo(int). Po jej wybraniu powinniśmy zobaczyć okno jak poniżej: Warto zauważyć, że jeżeli metoda opatrzona była komentarze to zostanie on przytoczony. Po podaniu wartości parametru aktualnego i wybraniu przycisku Ok, metoda zostanie wywołana, a wynik zostanie przedstawiony w takim samym oknie jak przy wywoływaniu metody statycznej (jego też możemy zapisać).

Przyjrzyjmy się teraz poleceniu Create Instance i stwórzmy instancję klasy Person korzystając z konstruktora dwuargumentowego. Po wybraniu konstruktora powinno pojawić się okno przedstawionej dalej: Po pierwsze powinniśmy podać nazwę dla tworzonego obiektu. W tym miejscu trzeba uważać bo jeśli podanym nazwę, której już użyliśmy zostaniemy o tym poinformowani dopiero po naciśnięciu przycisku Ok, a nie ma możliwości cofnięcia się i naprawienia błędu.

Po drugie, podobnie jak przy wywoływaniu metod, należy podać wartości poszczególnych argumentów. Napiszę o tym troszeczkę więcej. Jeśli chodzi o typy proste nie ma co się rozpisywać. Po prostu wpisujemy liczbę lub ciąg znaków (zamknięty w cudzysłowy) i koniec. Troszeczkę trudniej jest z typami złożonymi. Na rysunku widać, że dla parametru dateOfBirth wstawiłem dziwną wartość dateTime1. Niestety ale w przypadku typów złożonych musimy najpierw stworzyć instancję danego typu, a dopiero potem powołując się na przypisaną danej instancji nazwę, użyć jej jako argumentu wywołania. Nazwa stworzonej instancji pojawi się na rozwijanej liście dla argumentów o odpowiednim typie.

W tym przypadku stworzyłem uprzednio strukturę typu DateTime i nadałem jej nazwę dateTime1. Zacząłem od zlokalizowania typu struktury w drzewie (najwygodniej posłużyć się przyciskiem Search u góry okna Class View). Następnie wybrałem polecenie Create Instance, dalej zdecydowałem się na konstruktor trójargumentowy, jako argumenty wywołania podając rok, miesiąc i dzień mojego urodzenia (wszystkie wartości to typy proste). Na koniec powróciłem do klasy Person i ponownie wybrałem interesujący mnie konstruktor. Jak widać tworzenie obiektów wymagających skomplikowanej inicjalizacji jest trudne i kłopotliwe ale te okno nie do tego służy.

Po stworzeniu instancji klasy Person możemy przystąpić do testowania jej metod, a właściwie jednej metody. Odbywa się to dokładnie w ten sam sposób jak opisałem na przykładzie metody CompareTo(int). Polecam spróbować wywołać metodę GetAge(bool) z parametrem true oraz false oraz sprawdzić jaki wynik zwraca metoda statyczna GetCount() po utworzeniu instancji klasy Person.

Jeszcze dwie uwagi na koniec. Jeśli wprowadzimy jakieś zmiany do kodu to okno Object Test Bench wymusi na nas ponowną kompilację źródeł. Równoznaczne jest to z utratą wszystkich obiektów, struktur, które umieściliśmy wcześniej w tym oknie.

W drugiej części tej serii postów pisałem o oknie Immediate. W ciekawy sposób współpracuje ono z oknem Object Test Bench. Dla przypomnienia w oknie Immediate również możemy wywoływać metody i tworzyć obiekty. Istotne jest to, że obiekty, które stworzymy w tym oknie pojawią się również w oknie Object Test Bench. Tutaj też obowiązuje warunek, żeby użyty typ pochodził z projektu oznaczonego jako StartUp lub z pakietu/podzespołu, do którego tej projekt się odwołuje. Kolejny warunek jaki musimy spełnić (wygląda to na błąd w Visual Studio) jest taki, że okno Object Test Bench nie może być puste w momencie tworzenia obiektu w oknie Immediate. Jeśli będzie tworzone obiekty nie zostaną do niego dodane. Co ciekawe jeśli w pewnym momencie dodamy jakiś obiekt do okna Object Test Bench (z poziomu okna Class View) to nagle pojawią się w nim również obiekty utworzone wcześniej w oknie Immediate.

Warto jeszcze dodać, że obiekty z okna Immediate w oknie Object Test Bench będą miały takie same identyfikatory jak zmienne użyta w oknie Immediate. Czyli wpisanie takiej komendy:
Person person = new Person();
spowoduje dodanie do okna Object Test Bench obiektu o nazwie person

Opisane techniki testowałem w środowiskach Visual Studio 2005 oraz Visual Studio 2008.

01/02/2009

Awaria Google (część 3)

Home

I wszystko jasne. Zgodnie z komunikatem od Google, awarii zawinił pracownik, który przez przypadek zablokował wszystkie strony zamiast tylko tych z listy stron niebezpiecznych, które Google otrzymuje od serwisu StopBadware.org. Czyli jak się często zdarza najsłabszym ogniwem jest czynnik ludzki. Nie jestem zwolennikiem spiskowej teorii dziejów, więc takie wytłumaczenie jest dla mnie przekonujące. Zresztą czas to zweryfikuje. Przy tej okazji przypomniały mi się dwie zasłyszane kiedyś historie w tym klimacie.

Dla ustalenia uwagi wyobraźmy sobie niezwykle ważny dla jakiejś firmy serwer. Na serwerze przechowywane są bardzo istotne dane, musi on być dostępny on-line 24 godziny na dobę, chroniony jest najnowszym oprogramowaniem antywirusowym itd. Ale jak w każdej firmie ktoś musi odkurzać, wynosić śmiecie, zmywać podłogi, również w serwerowni. Nie ulega wątpliwości, że leżące na ziemi kable zasilająca mogą utrudnić zadanie sprzątaczce czy też sprzątaczowi. Co "powinien", więc zrobić wykonujący skrupulatnie i dokładnie swoją pracę konserwator powierzchni płaskich? ... Od tego czasu we wspomnianej firmie kable zasilające są na stałe przymocowane do gniazdek.

Druga historia dotyczy pewnego administratora sieci, który jako wygaszacz ekranu na serwerze postanowił użyć słynnego "blue screen". Niestety ale zdarzyło się, że koło serwera przechodził pewien menadżer. Kiedy zobaczył wygaszacz ekranu postanowił wyręczyć administratora w jego pracy i zrestartował serwer. Dodam, że nagły restart tego serwera nie był bez znaczenia dla firmy i jego klientów. Ciekawe kto stracił pracę?

Podsumowując, bez względu na to jakich zabezpieczeń użyjemy, zawsze zapomnimy o jakimś małym szczególe lub nie uwzględnimy czegoś co będzie wydawać się nam absurdalnie niemożliwe, a co doprowadzi do mniejszej lub większej katastrofy.

Swoją drogą zastanawialiście się kiedyś jak to jest, że jeżeli pracownicy danej firmy mają ograniczony dostęp do poszczególnych pomieszczeń to sprzątaczka czy sprzątacz mogą wejść wszędzie (lub prawie wszędzie). No chyba, że prezes będzie sprzątać swoje biuro samemu ;)