19/03/2011

Jak właściwość może napsuć krwi

Home

Właściwości to jedna z moich ulubionych cech języka C#. W poście tym nie będę jednak przekonywał opornych, jeśli tacy są, do ich używania. Opiszę natomiast przypadek złego wykorzystania właściwości, który ostatnio napsuł mi trochę krwi. Załóżmy, że mamy aplikację WWW lub grubego klienta. Aplikacja ta komunikuje się z bazą danych za pośrednictwem tzw. dostawców danych czyli klas implementujących dobrze określone interfejsy. W zależności od konfiguracji dostawca może być dostępny lokalnie w ramach procesu aplikacji lub zdalnie czyli być uruchomiony na innej maszynie np. jako usługa WCF. Z perspektywy aplikacji jest to przezroczyste.

Dla ustalenia uwagi przyjmijmy, że dostawca udostępnia metodę Customer GetCustomer(int customerId), która zwraca obiekt klasy Customer reprezentujący klienta o podanym identyfikatorze. Problem polega na tym, że w zależności od tego czy dostawca danych działa lokalnie czy zdalnie to dla tego samego identyfikatora zwraca trochę inne dane. Dokładniej mówiąc jeśli mamy do czynienia z dostawcą lokalnym wszystko jest w porządku. Natomiast dla dostawcy zdalnego zwracane dane są niekompletne w tym sensie, że nie wszystkie właściwości obiektu Customer są ustawione. Co ciekawe po zdebugowaniu procesu serwera okazuje się, że zwracane przed dostawcę dane są poprawne ale po dotarciu do aplikacji okazują się jednak niekompletne.

Początkowo przecierałem oczy ze zdziwienia bo jak to możliwe, że część danych nagle znika. Po zagłębieniu się w problem znalazłem jednak winnego, którym okazała się błędne zaimplementowana właściwość w połączeniu z serializacją XML'ową. Istotę problemu przedstawia poniższy kod:

private string _StringValue;
public string StringValue
{
  get
  {
    if(_StringValue == "aaa")
      return "Ala ma kota";
    else if(_StringValue == "bbb")
      return "Kot ma Alę";
    ...
    else
      return null;
  }
  set
  {
    _StringValue = value;
  } 
}

Jak widać wartość zwracana przez getter właściwości jest inna niż wartość ustawiana przy pomocy settera. W przypadku kiedy dostawca działa lokalnie nie stanowi to żadnego problemu ponieważ pracujemy bezpośrednio z klasą dostawcy danych. W przypadku kiedy dostawca uruchomiony jest zdalnie to w rzeczywistości po stronie aplikacji pracujemy z obiektem proxy. W momencie wywołania metody GetCustomer do serwera wysyłane jest odpowiedni komunikat i wywoływana jest "prawdziwa" metoda GetCustomer. W wyniku otrzymujemy obiekt klasy Customer, która musi zostać przesłany do proxy. W tym celu obiekt musi zostać zserializowany czyli zapisany do strumienia i wysłany przez sieć.

I tutaj jest pies pogrzebany. Jeśli zostanie użyta serializacja XML'owa czyli taka, która "zamieni" obiekt na dokument XML to do proxy zostanie wysłana wartość zwrócona przez getter właściwości czyli coś innego niż wartość, która została ustawiona przy pomocy settera np.: jeśli do ustawienia właściwości StringValue użyjemy łańcucha "aaa" to w dokumencie XML zostanie zapisana wartość "Ala ma kota".

Po odebraniu odpowiedzi proxy spróbuje odtworzyć obiekt na podstawie odebranego dokumentu XML. Do zainicjowania właściwości StringValue użyje wartości z dokumentu czyli łańcucha "Ala ma kota". Teraz jeśli odczytamy właściwości StringValue to dostaniemy null ponieważ kod w getterze nie rozpozna łańcucha "Ala ma kota". Jeśli użyjemy serializacji binarnej problem nie wystąpi ponieważ w strumieniu zostanie zapisana wartość prywatnego pola _StringValue. Jeśli chcemy pozostać przy serializacji XML'owej to należy trochę zmodyfikować kod. Właściwość StringValue powinna być tylko do odczytu. Należy również stworzyć dodatkową właściwość do ustawiania danych.

public string StringValueForSerialization
{
  get; set;
}

public string StringValue
{
  get
  {
    if(StringValueForSerialization == "aaa")
      return "Ala ma kota";
    else if(StringValueForSerialization == "bbb")
      return "Kot ma Alę";
    ...
    else
      return null;
  }
}


11/03/2011

Kilka słów o operatorach

Home

Dzisiaj kolega podesłał mi swoistą zagadkę w postaci dwóch, pozornie robiących to samo, fragmentów kodu. Jeden napisany w C#, a drugi w VB.NET:
int counter = 0;
while (++counter < 10)
{
  Console.WriteLine(counter.ToString());
}
Dim counter As Integer = 0
While (++counter < 10)
  Console.WriteLine(counter.ToString())
End While
Różnica polega na tym, że pętla w C# wykona się 9 razy, a w VB.NET otrzymamy pętlę nieskończoną. Dlaczego? Przyjrzyjmy się dokładniej pętli napisanej w języku VB.NET. Ci z was, którzy programują lub programowali w tym języku zapewne zwrócili uwagę na użycia operatora preinkrementacji. No właśnie, od kiedy w VB.NET mamy operator preinkrementacji? I tu tkwi pies pogrzebany, w VB.NET nie ma operatora preinkrementacji czy też predekrementacji. W takim razie czemu ten kod w ogóle się kompiluje? Początkowo pomyślałem, że to błąd kompilatora. W przekonaniu tym utwierdził mnie dodatkowo reflector, który po wrzuceniu do niego dll'ki pokazał coś takiego:
...
Do While (counter < 10)
...
Efekt zaobserwowałem w VS 2010, a więc eksperyment przeprowadziłem jeszcze w VS 2008 z tym samym wynikiem. W tym momencie pomyślałem, że to jednak niemożliwe aby przez tyle lat taki bug pozostał niewykryty. Po chwili mnie olśniło i wszystko okazało się proste i oczywiste. Zanim rozwiążę zagadkę do końca powiem, że poniższy kod również się skompiluje:
Dim counter As Integer = 0

While (++-+---++--+-+-counter < 10)
  Console.WriteLine(counter.ToString())
End While
C# nie jest gorszy i w nim również da się stworzyć podobnego dziwoląga:
int counter = 0;
while (+-+-+-+-+--counter < 10)
{
  Console.WriteLine(counter.ToString());
}
W tej chwili wszystko powinno być już jasne. + i - to nie tylko operatory binarne ale również operatory unarne. Poniżej fragment dokumentacji z MSDN:

Unary + operators are predefined for all numeric types. The result of a unary + operation on a numeric type is just the value of the operand.

Unary - operators are predefined for all numeric types. The result of a unary - operation on a numeric type is the numeric negation of the operand.

W języku VB.NET, tak jak pisałem wcześniej, nie ma operatorów preinkrementacji oraz predekrementacji, a więc zmienną liczbową możemy poprzedzić dowolną kombinacją + i -. W języku C# takie operatory istnieją, a więc zmienna liczbowa może zostać poprzedzona ciągiem + i - pod warunkiem, że występują one naprzemiennie (z wyjątkiem ostatniej pary).


26/02/2011

Jak zagłodzić Timer?

Home

Okazuje się, że bardzo prosto, ale zacznijmy od początku. Niedawno zakończyłem pracę nad serwerem zajmującym się wykonywaniem tzw. zadań wsadowych. Definicje zadań do wykonania pobierane są z bazy danych, a w danym momencie może działać wiele serwerów. Każdy serwer rezerwuje sobie swoje zadania na pewien kwant czasu. Po upływie tego czasu inne serwery mają prawo przejąć to zadanie. Może się tak zdarzyć na przykład jeśli jakiś serwer ulegnie awarii. Jeśli wykonanie danego zadania zajmuje więcej czasu niż czas rezerwacji to serwer musi przedłużyć dzierżawę.

W tym celu stworzyłem komponent, który monitoruje zadania przetwarzane przez serwer i kiedy zbliża się czas wygaśnięcia dzierżawy, przedłuża ją. Komponent ten korzysta z klasy Timer z przestrzeni nazw System.Timers, która co określony kwant czasu generuje zdarzenie. W metodzie obsługującej to zdarzenia nie robię nic innego jak po prostu aktualizuję czas wygaśnięcia dzierżawy. Tyle w telegraficznym skrócie.

W czasie testowania stworzonego rozwiązania zauważyłem, że w niektórych przypadkach czas wygaśnięcia dzierżawy nie jest aktualizowany. Wyglądało na to jakby klasa Timer nie generowała zdarzeń albo generowała je z dużym opóźnieniem! Wbrew pozorom rozwiązanie zagadki okazało się bardzo proste. Otóż klasa Timer generuje zdarzenie Elapse w wątku pochodzącym z puli wątków ThreadPool. Jeśli dodamy do tego fakt, że zadania wsadowe również są wykonywane przez wątki z puli to wszystko staje się oczywiste. Jeśli serwer umieści w puli odpowiednio dużo zadań wsadowych to może okazać sie, że brakuje wątków dla klasy Timer.

Poniżej prosty kod prezentujący ten efekt. Na początku ograniczam liczbę wątków do 10 i zlecam wykonanie 10 zadań. Zanim wszystkie zadania zostaną uruchomione na ekran zostanie wypisanych kilka napisów Hello World from System.Timers.Timer!. Następnie kiedy wszystkie 10 zadań zostanie uruchomionych napis przestanie się pokazywać. Zobaczymy go ponownie kiedy przynajmniej jedno zadanie zostanie zakończone i tym samym zwolni sie jeden wątek.
class Program
{
  static void Main(string[] args)
  {
    ThreadPool.SetMaxThreads(10, 10);
    
    System.Timers.Timer t = new System.Timers.Timer();
    t.Interval = 1000;
    t.Elapsed += new System.Timers.ElapsedEventHandler(t_Elapsed);
    t.Start();

    for (int i = 0; i < 10; ++i)
        ThreadPool.QueueUserWorkItem((o) =>
            {
              Console.WriteLine("Start " + o);
              Thread.Sleep(10000);
              Console.WriteLine("End " + o);
            }, i);

    Console.ReadLine();
  }

  static void t_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
  {
    Console.WriteLine("Hello World from System.Timers.Timer!");
  }
}
Taki sam efekt otrzymamy jeśli użyjemy klasy System.Threading.Timer zamiast System.Timers.Timer. Jak można poradzić sobie z tym problemem. Uważam, że są trzy rozwiązania:
  • Zliczać liczbę zadań umieszczanych w puli wątków i pilnować aby zawsze, przynajmniej jeden wątek z puli był wolny i mógł być użyty przez klasę Timer. Maksymalną liczbę wątków możemy pobrać przy pomocy metody GetMaxThreads. Jest to jednak podejście sprzeczne z ideą puli wątków, do której wrzucamy zadania do wykonania i nie zastanawiamy się kiedy dokładnie zostanie uruchomione, przez jaki wątek itd.
  • Zrezygnować z klasy ThreadPool i samemu zarządzać wątkami.
  • Użyć (napisać) zegar odpalający zdarzenia Elapse poza pulą wątków.
W opisie problemu pominąłem Task Parallel Library ponieważ w omawianym przypadku użycie .NET 4.0 nie było możliwe. Szybki test pokazał jednak, że jeśli mógłbym użyć Task Parallel Library to problem nie wystąpiłby. Z tego co wiem Task Parallel Library nie używa klasy ThreadPool, a więc przyczyna problemu nie występuje.

21/02/2011

Domeny globalne

Home

Od kilku miesięcy jestem szczęśliwym posiadaczem własnej domeny michalkomorowski.com. Jakiś czas temu moje szczęście zostało jednak zakłócone kiedy dowiedziałem się, że usługa whois udostępnia publiczne moje pełne dane adresowe!!! Szybko skontaktowałem się z firmą home.pl, za pośrednictwem której wykupiłem domenę aby wyjaśnić sprawę.

Co tu dużo mówić byłem nastawiony wojowniczo ale mój zapał został szybko zgaszony. Otóż okazało się, że dla domen globalnych dane właścicieli są domyślnie jawne. Wymaganie takie stawia rejestratorom organizacja ICANN czyli główny zarządca domen na świecie. W regulaminie znajduje się oczywiście stosowny punkt, który przeoczyłem przeglądając umowę. Szczęście w nieszczęściu aby ukryć dane wystarczy wypełnić elektroniczny formularz. Po jego wypełnieniu moje dane zostały ukryte w ciągu 24 godzin, tutaj plus dla home.pl za szybką reakcję.

Z drugiej strony sądzę, że w dobrym tonie byłoby gdyby firmy zajmujące się rejestracją domen informowały o takich rzeczach przy pomocy dużego czerwonego napisu ponieważ sprawa nie jest oczywista. Z ciekawości sprawdziłem kilka domen globalnych dla polskich blogów i dla większości z nich wynik zwrócony przez usługę whois, zawierał dane adresowe właścicieli. Może sie mylę ale z dużą pewnością ich autorzy oraz wielu innych nie zdaje sobie z tego sprawy.

16/02/2011

DockPanel i ScrollViewer

Home

Dlaczego lubię kontener DockPanel? Ponieważ dobrze (intuicyjnie) współpracuje z kontrolką ScrollViewer, która dostarcza pionowych i poziomych pasków przewijania. Załóżmy, że główne okno naszej aplikacji zawiera listę (kontrolka ListView) z kilkuset wierszami. Wierszy jest na tyle dużo, że w danym momencie na ekranie widoczna jest tylko część z nich. Aby użytkownik mógł przewijać listę i zobaczyć wszystkie wiersze używamy kontrolki ScrollViewer

ScrollViewer zachowa się jednak inaczej w zależności od kontenera w jakim zostanie osadzony. Jeśli będzie to np.: StackPanel lub Grid to paski przewijania nie zostaną wyświetlone i otrzymamy efekt jak poniżej.



Można sobie z tym poradzić ustawiając na kontrolce ScrollViewer właściwość Height ale spowoduje to, że lista będzie miała zawsze tą samą wysokość bez względu na wielkość okna. Podobnie można ustawić właściwość Height dla listy ale efekt będzie taki sam.



Dużo lepszym rozwiązaniem jest osadzenia kontrolki ScrollViewer wewnątrz kontenera DockPanel. Dzięki temu uzyskamy efekt jak poniżej. Paski przewijania są widoczne, a wysokość listy zależy od wielkości okna.