08/03/2009

Prymitywy synchronizacyjne - jak dobrze je znamy?

Home

Ostatnio zadałem kilku osobom następujące pytanie. Spośród wymienionych poniżej prymitywów synchronizacyjnych wybierz, te które mogą zostać użyte do synchornizacji między-procesowej:
  • Klasa Monitor
  • Klasa Mutex
  • Klasa Semaphore
  • Słowo kluczowe lock
  • Klasa AutoResetEvent
  • Klasa ManualResetEvent
Sądziłem, że pytanie należy raczej do tych z kategorii łatwiejszych. Odpowiedź na nie przysporzyła jednak niestety sporo kłopotów. Na poniższej liście zostawiłem tylko elementy stanowiące poprawną odpowiedź:
  • Klasa Mutex
  • Klasa Semaphore
  • Klasa AutoResetEvent
  • Klasa ManualResetEvent
Czym różnią się wybrane prymitywy synchronizacyjne od pozostałych? Moim zdaniem można wymienić cztery zasadnicze różnice:
  • Po pierwsze klasy te dziedziczą pośrednio po klasie MarshalByRefObject i w związku z tym instancje tych klas mogą przekraczać granice po między dziedzinami aplikacyjnymi (ang. application domain).
  • Po drugie klasy te są wywiedzione z klasy WaitHandle i w związku można ich używać w podobny sposób chociaż mają zupełnie inną semantykę. W szczególności można sobie wyobrazić scenariusz, w którym wątek oczekuje równocześnie na zwolnienie semafora, mutexa i zapalenie się sygnału/zdarzenia (należy użyć metody statycznej WaitHandle.WaitAll())
  • Po trzecie klasy te stanowią w rzeczywistości opakowanie na natywne/systemowe prymitywy synchronizacyjne udostępnione w systemie operacyjny co generalnie powoduje, że są wolniejsze.
  • Po czwarte i najważniejsze z perspektywy pytania te cztery prymitywy synchronizacyjne mogą zostać nazwane. Inaczej mówiąc, w jednym procesie możemy utworzyć semafor o nazwie "MY SEMAPHORE", a w drugim procesie uzyskać dostęp do tego samego semafora posługując się podaną nazwą. W tym celu można użyć metody statycznej Semaphore.OpenExisting() lub z odpowiedniego konstruktora.
Moim zdaniem wiedza o sposobie działania, semantyce poszczególnych prymitywów synchronizacyjnych jest niezwykle ważna dla każdego programisty bez względu z jaką technologią pracuje. Błędy związane z synchronizacją są jednymi z najtrudniejszych do wykrycia i naprawienia, a wiele z nich można by unikać gdyby wszyscy programiści znali dokładnie narzędzie swojej pracy.

05/03/2009

Xceed Chart

Home

Ostatnie kilka dni w pracy spędziłem na oprogramowywaniu komponentu do rysowania wykresów firmy Xceed w wersji dla Windows Forms (jest też wersja dla ASP.NET). Wcześniej nie korzystałem z produktów tej firmy i po tych kilku dniach kontaktu z jednym z nim muszę przyznać, że pracowało mi się bardzo przyjemnie.

API nie jest bardzo trudne, a jeśli sprawia trudności zawsze można sięgnąć do bogatej biblioteki interaktywnych przykładów. Komponent wręcz przytłacza swoimi możliwościami. Mam na myśli naprawdę przeogromną liczbę opcji konfiguracyjnych. Wybierać możemy pomiędzy wykresami dwu- i trój- wymiarowymi, liniowymi, słupkowymi, radarowymi... i prawie dowolnie dostosowywać ich wygląd do swoich potrzeb. Podczas pracy komponent zachowywał się bardzo stabilnie. Jak do tej pory nie zauważyłem również problemów z wydajnością. Nadmienię jeszcze, że pracowałem z starszą wersją komponentu (4.0). Zakładam, że wersja najnowsza (4.2) musi być co najmniej tak dobra jak stara.

Jeśli ktoś potrzebuje kontrolki do rysowania wykresów to naprawdę polecam. Największy mankament to niestety cena - kilkaset dolarów. Do użytku osobistego drogo ale dla firmy nie jest to już duży wydatek.

22/02/2009

Zakamarki Visual Studio (cz. 4)

Home

W czwartym już poście dotyczącym różnych ciekawych funkcji środowiska programistycznego Visual Studio chciałbym przyjrzeć się dokładniej oknu Call Stack, a przy okazji poruszyć kilka innych tematów.

Okno Call Stack, breakpoint'y...

Czym jest breakpoint zapewne każdy programista wie, a jeśli nie to wstyd. Z drugiej strony nie jestem już taki pewny czy każdy programista zna wszystkie możliwości breakpoint'ów w Visual Studio: filtrowanie, warunki, zliczanie, uruchamiania makr czy wstawienie breakpoint'a do metody, której kodu nie znamy (o tym pisałem tutaj). Opisanie tych rzeczy to temat na oddzielny post. W tym miejscu skupmy się na użyciu pułapek w oknie Call Stack.

Po pierwsze umożliwia ono wstawianie breakpoint'ów. W tym celu wybieramy interesującą nas ramkę (metodę), zaznaczamy ją i naciskamy przycisk F9 (albo wywołujemy menu kontekstowe i wybieramy Breakpoint -> Insert Breakpoint). Breakpoint pojawi się zarówno w oknie Call Stack jak i na marginesie edytora kodu. W odwrotnym scenariuszu breakpoint postawiony w kodzie pojawi się w oknie Call Stack dopiero kiedy spowoduje zatrzymanie aplikacji. W każdej metodzie może zostać umieszczony wiele pułapek i trudno by je wszystkie umiescić w oknie Call Stack.

Przy pomocy okna Call Stack można również zarządzać pułapkami czyli określić warunek zatrzymania, filtr, wyłączyć pułapkę itd. W tym celu klikamy prawym przyciskiem wybraną pułapkę, z menu kontekstowego wybieramy Breakpoint, dalej interesującą nas komendę np.: Disable Breakpoint w celu wyłączenia pułapki lub Condition... w celu określenia warunku zatrzymania się pułapki.



Okno Call Stack umożliwia również ustawienie punktu śledzenia (ang. tracepoint). Tracepoint to w gruncie rzeczy specjalny breakpoint, który nie powoduje zatrzymania wykonania ale wypisanie komunikatu do okna Output. W celu ustawienie tracepoint z poziomu okna Call Stack wywołujemy menu kontekstowe i wybieramy Breakpoint -> Insert Tracepoint. Ustawienie punktu śledzienia z poziomu edytora kodu rozpoczyna się od ustawienia zwykłego brakpoint'a. Następnie należy wywołać menu kontekstowe, dalej wybrać When Hit... (sposób ten działa również w przypadku okna Call Stack). W oknie, które pojawi się zaznaczamy pola wyboru jak na rysunku:

Jeśli nie zaznaczymy pola wyboru Continue execution utworzymy zwykły breakpoint, który dodatkowo będzie powodował wypisanie komunikatu do okna Output. Tracepoint'y reprezentowane są przez romby:

W polu tekstowym okna When Breakpoint Is Hit możemy wpisać dowolny tekst, wyświetlić dowolną zmienną lub użyć jednego ze specjalny słów kluczowych. Ale po kolei. Aby wyświetlić zmienną należy wpisać { nazwa }. Słowa kluczowe wpisujemy natomiast po znaku $. Na przykład $PID służy do wypisania identyfikatora procesu. Nie będę omawiał wszystkich słów kluczowych ponieważ, jak widać na powyższym rysunku, ich opis można znaleźć w oknie When Breakpoint Is Hit. Zwrócę tylko jeszcze uwagę na bardzo ciekawą możliwość wyświetlenia stosu przy pomocy słowa kluczowego $CALLSTACK oraz na to, że przy tworzeniu komunikatu jesteśmy ograniczeni tylko do jednej linii.

Run To Cursor

Run To Cursor to bardzo znane polecenie, które umożliwia rozpoczęcie debugowania i wskazanie miejsca w kodzie, w którym debugger ma się zatrzymać (pod warunkiem, że nie zatrzyma się wcześniej z powodu breakpoint'a). Niewiele osób jednak wie, że polecenie to jest dostępna z poziomu okna Call Stack. Pozwala wskazać, do którego miejsca chcemy zwinąć stos. Wystarczy wybrać interesującą nas ramkę/metodę i z menu kontekstowego wybrać tę komendę.

Wartość argumentów

Inną bardzo fajną cechą okna Call Stack jest to, że podaje ono wartości przekazanych do funckji argumentów. Jeśli wartości argumentów nie są widoczne powinniśmy w onie Call Stack wywołać menu kontekstowe, a następnie zaznaczyć Show Parameter Values. No dobrze, w przypadku typów prostych jest to z pewnością przydatne ale co z typami złożonymi. Domyślnie zostanie wyświetlona po prostu nazwa typu ale można to zmienić korzystając z atrybutu DebuggerDisplayAttribute. Dla tych co nie wiedzą jest to jeden z wielu atrybutów, który pozwala na dostosowanie debuggera do naszych potrzeb (kolejny dobry temat na oddzielny post, a nawet kilka :)). Najłatwiej wyjaśnić to na przykładzie. Posłużmy się kodem jak poniżej:
public class Test
{
   private int value;

   public int Value
   {
      get { return this.value; }
   }

   public Test(int value)
   {
      this.value = value;
   }
}
Wartości typu Test zostaną przedstawione w taki o to mało interesujący sposób:

ConsoleApplication.Program.Equals(a = {ConsoleApplication.Program.Test}, b = {ConsoleApplication.Program.Test})

Wystarczy jednak zmodyfikować definicję klasy Test jak poniżej:
[DebuggerDisplay("Value = {value}")]
public class Test
{
...
}
Aby osiągnąć taki efekt:

ConsoleApplication.Program.Equals(a = Value = 1, b = Value = 1

Technika ta działa nie tylko w przypadku okna Call Stack ale również w przypadku innych okien debuggera czyli np.: Quick Watch.

Unwind To This Frame

Unwind To This Frame to jedna z komend dostępna w menu kontekstowym okna Call Stack. W większości przypadków kiedy wywołamy menu jest wyszarzona. Dostępna staje się tylko w przypadku kiedy zostanie rzucony wyjątek. W momencie rzucenia wyjątki środowisko najczęściej zatrzymuje wskazując linię w kodzie, w której wyjątek został wygenerowany (w szczególności może się nie zatrzymać jeśli wyjątek został obsłużony, a my nie mamy zaznaczonej opcji zatrzymywania na wszystkich wyjątkach). W oknie Call Stack ramka, w której został rzucony wyjątek wskazywana jest przy pomocy żółtej strzałki. Jeśli wyjątek został rzucony poza naszym kodem ostatnia ramka wskazująca nasz kod zostanie oznaczona strzałką zieloną .

Kiedy środowiska zatrzyma się już z powodu wyjątku możemy w oknie Call Stack wybrać interesującą nas ramkę, wywołać menu i wybrać komendę Unwind To This Frame. Spowoduje to zwinięcie stosu do wybranej ramki (działanie podobne do Run To Cursor) i umożliwi np.: zdiagnozowanie przyczyny wyjątku, modyfikację zmiennych lokalnych tak aby wyjątek nie został rzucony itd.

Polecenie Unwind To This Frame nie jest dostępne dla wszystkich ramek. Stos można zwinąć do każdej ramki powyżej ostatniej ramki, która wskazuje miejsce w naszym kodzie (pomiędzy żółtą i zieloną strzałką :)). Czyli jeśli wyjątek został rzucony bezpośrednio w naszym kodzie polecenie będzie dostępne tylko dla jednej ramki. Z kolei dla przypadku pokazanego niżej:

System.Xml.Serialization.XmlSerializer.Deserialize(xmlReader, encodingStyle, events)
System.Xml.Serialization.XmlSerializer.Deserialize(stream)
ConsoleTest.Program.Main(args)
...


Stos możemy zwinąć do jednej z metod Deserialize() albo do metody Main() w naszym kodzie.

Omawiając tą funkcjonalność należy koniecznie napisać o exception assistant. Całkiem możliwe, że większość użytkowników Visual Studio nigdy o nim nie słyszała. W sumie nie dziwne ponieważ jest domyślnie włączony (Tools -> Options -> Debugging -> General -> Enable the exception assistant). A do czego służy? Poniżej zamieszczam okienka wyświetlone przez środowisk w momencie rzucenia wyjątku kiedy asystent jest wyłączony (pierwszy obrazek) i kiedy jest włączony (drugi obrazek):



Mam nadzieję, że zalety asystenta są oczywiste :) ale asystent to nie tylko ładne okienko. Jeśli jest włączony w momencie rzucenia wyjątki środowisko wskaże zawsze linię w naszym kodzie nawet jeżeli prawdziwe źródło wyjątku jest gdzie indziej. Dzięki temu możemy po prostu "złapać" żółtą strzałkę, wskazującą w głównym oknie ostatnio wykonaną instrukcję, i przeciągnąć ją kilka linijek wcześniej lub kilka linijek dalej. Innymi słowy możemy ominąć wyjątek lub prześledzić jak został spowodowany. Przy włączonym asystencie polecenie Unwind To This Frame staje się trochę mniej ponieważ przydatne ale czasami możemy chcieć wyłączyć asystenta. W każdym razie dobrze zdawać sobie sprawę z jego istnienia.

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

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