Artykuł

wrz 08 2012
0

Wielowątkowość w C# - klasa Timer

Jednym z bardziej charakterystycznych zadań, którego implementację można wykazać w szeregu różnych aplikacji dostępnych na rynku, jest praca wykonywana w stałych określonych interwałach czasu. Przykładów nie trzeba daleko szukać, ponieważ wystarczy spojrzeć choćby na aplikacje mobilne, o których trochę na tym blogu się pisze. W przypadku smartfonów, zjawisko to widoczne jest w aspekcie synchronizacji. Sprawdź statusy na Facebooku co 3 godziny, ściągnij wiadomości RSS co 6 godzin, aktualizuj kalendarz co godzinę itd. Powyższe trzy rozwiązania to tylko kropla w morzu synchronizacji, które nas otacza.

Co łączy zatem wszystkie tego typu przykłady? Oczywiście wspomniana częściowo wcześniej praca interwałowa - czyli wykonywanie określonych stałych operacji w z góry zdefiniowanym, stałym odstępie czasu. A jak można realizować tego typu zadania? Pomocny w tym przypadku okaże się tytułowy Timer.

W tym miejscu należy się Wam jednak mała istotna uwaga. Klasa Timer występuje w C# trzykrotnie. My zajmiemy się opisem implementacji Timera z przestrzeni nazw System.Timers oraz System.Threading.

System.Timers.Timer oraz System.Threading.Timer - różnice

Tak jak wspomniałem we wstępie, w dzisiejszym wpisie zajmiemy się dwoma z trzech implementacji Timera, która może znaleźć we Frameworku .Net. Istnieje między nimi kilka istotnych różnic, zaczniemy jednak od małego paradoksu. Wydawałoby się że to Timer, który jest zawarty w przestrzeni nazw System.Threading będzie zabezpieczony przed wielowątkowością (z ang. Thread-Safe) w istocie jednak takich zabezpieczeń on nie posiada. Bezpieczny jest za to, ten który został umieszczony w przestrzeni nazw System.Timers na co z pozoru nic nie wskazuje.

Ponieważ ciągłe porównywanie kluczowych cech obu klas zawartych w różnych przestrzeniach nazw byłoby meczące, postanowiłem umieścić kluczowe różnice w poniższej tabelce.

Cecha System.Timers.Timer System.Threading.Timer
Thread-safe Tak Nie
Przyjazny model obiektowy Tak Nie
Obsługa obiektu stanu Nie Tak
Możliwość zaplanowania zdarzenia przy uruchomieniu Nie Tak
Możliwość rozszerzania klasy Tak Nie

Myślę, że zawartość tabeli jest na tyle jasna, że nie wymaga ona żadnego dodatkowego komentarza. Oczywiście różnica wystąpi również w samej implementacji, choć pojawiają się również pewne elementy wspólne dla obu klas, co najlepiej wykażą poniższe przykłady.

System.Timers.Timer - przykład implementacji

Żeby zobrazować działanie Timera, użyjemy prostego przykładu, który co 1000 milisekund wyświetlać będzie liczbę milisekund, które upłynęły od czasu uruchomienia aplikacji. Formalnie w tym celu wykorzystamy właściwość TickCount klasy Environment, która przechowuje liczbę milisekund, które upłynęły od momentu uruchomienia systemu. Jej wartość początkową (po uruchomieniu aplikacji) zapiszemy do zmiennej. Dzięki temu będziemy mogli obliczyć dokładny czas, który upłynął od momentu uruchomienia naszej aplikacji.

Spójrzmy na kompletny listing kodu:

using System;
using System.Timers;

namespace ThreadsTimer
{
    class Program
    {
        static void Main(string[] args)
        {
            MyTimer oTimer = new MyTimer();
            oTimer.StartTimer();
        }
    }

    class MyTimer
    {
        private int m_nStart = 0;

        public void StartTimer()
        {
            m_nStart = Environment.TickCount;
            Timer oTimer = new Timer();
            oTimer.Elapsed += new ElapsedEventHandler(OnTimeEvent);
            oTimer.Interval = 1000;
            oTimer.Enabled = true;
            Console.WriteLine(
                "Naciśnij dowolny przycisk, aby zakończyć program.");
            Console.Read();
            oTimer.Stop();
        }


        private void OnTimeEvent(object oSource,
            ElapsedEventArgs oElapsedEventArgs)
        {
            Console.WriteLine("Upłyneło {0} milisekud",
                Environment.TickCount - m_nStart);
        }
    }
}

Czas na małą analizę. W głównej klasie naszej aplikacji, zasadniczo dzieje się nie wiele. Właściwie uruchamia ona tylko naszą klasę testową. Kluczowe działania zostały zawarte w klasie MyTimer. Zgodnie z tym co pisałem wcześniej, klasa posiada pole typu integer (17), w którym zapisujemy informacje o początkowej wartości właściwości Environment.TickCount. Oprócz tego znajdziemy tutaj dwie metody. Metoda StartTimer robi dokładnie to co mówi jej nazwa i poświęcimy jej kilka zdań już za chwilę. Druga z metod - OnTimeEvent obsługuje zdarzenie Elapsed naszego Timera (musi mieć ona odpowiednio zgodną sygnaturę). Zdarzenie to ma miejsce w sytuacji gdy upłynie określony w Timerze interwał czasu pomiędzy kolejnymi jego iteracjami. W naszym przypadku metoda OnTimeEvent jest bardzo prosta i wyświetla informacje (36) o czasie który upłynął od momentu uruchomienia aplikacji o czym wspominałem już wcześniej.

Wróćmy teraz do metody StartTimer. Na wstępie ustawiamy naszą zmienną (21), a następnie przystępujemy do tworzenia jego obiektu. W kolejnym liniach ustawiamy kolejne jego właściwości i zdarzenia:

  • Przypisujemy metodę OnTimeEvent do zdarzenia Elapsed (23)
  • Określamy interwał pomiędzy kolejnymi operacjami jako 1000 milisekund (24; pamiętajcie w tym momencie o jednostkach - to bardzo ważne)
  • Aktywujemy Timer za pomocą flagi Enabled (25)

Później oczekujemy na reakcję użytkownika - tj. naciśnięcie dowolnego przycisku (gdybyśmy nie zastosowali tricku z Console.Read to aplikacja zakończyłaby się zaraz po uruchomieniu, a my nie zobaczylibyśmy właściwie żadnego efektu jej pracy). Aplikacja zatrzyma Timer jak tylko użytkownik naciśnie dowolny klawisz. Formalnie rzec biorąc metoda Stop w tym konkretnym przypadku jest zbędna (program zakończył by i tak swoje działanie), jednak jest to dobra praktyka, która pozwala zapanować nad porządkiem w kodzie oraz w samej aplikacji.

Jeśli uruchomicie przykład, to z pewnością może Was zaskoczyć pewna różnica w dokładności uzyskiwanych rezultatów. Nie będzie to raczej 1000,2000,3000 itd. Jest to po części związane z przetwarzaniem napisanego przez nas kodu oraz sposobem pobierania informacji o czasie.

Przy okazji warto jeszcze wspomnieć o znaczeniu właściwości Enabled. Ustawienie jej wartości na True, jest tożsame z wywołaniem metody Start, natomiast False jest dokładnie tym samym co metoda Stop. Wybór działania zależy już tylko od Was;)

System.Threading.Timer - przykład implementacji

Tak jak wspomniałem wcześniej, implementacja tego Timera różni się trochę od tego, co mieliśmy okazję zobaczyć wyżej. Przede wszystkim, wszystkie niezbędne informacje określamy już w samym konstruktorze, który posiada aż 5 różnych postaci.

Pozytywnym aspektem tego Timera jest to, że możemy korzystać również z wartości zapisanych jako long, uint bądź też TimeSpan. W tym przypadku możliwe jest również wywołanie określonej akcji już w momencie uruchomienia Timera (jest to szczególnie istotne jeśli interwał pomiędzy kolejnymi wywołaniami jest duży). Ten element konstruktora można wykorzystać również do planowego uruchomienia Timera o zadanym czasie. Może to być szczególnie przydatne w kontekście usług systemowych, które muszą rozpocząć pewne działania (np. synchronizację) od z góry określonej pory.

W przypadku wątkowego Timera, nasza metoda wykonująca operacje pomiędzy interwałami czasu również musi posiadać określoną sygnaturę. W tym przypadku przyjmuje ona tzw. obiekt stanu, którym może być dowolny obiekt (w tym również taki napisany przez nas samych). Osobiście dla prostoty skorzystałem z napisu pobranego od użytkownika w konsoli. Spójrzcie na przykład:

using System;
using System.Threading;

namespace ThreadsTimer
{
    class Program
    {
        static void Main(string[] args)
        {
            MyTimer oTimer = new MyTimer();
            oTimer.StartTimer();
        }
    }

    class MyTimer
    {
        private int m_nStart = 0;

        public void StartTimer()
        {
            m_nStart = Environment.TickCount;
            Console.WriteLine("Wprowadź swoje imię i naciśnij Enter:");
            string sName = Console.ReadLine();
            Timer oTimer = new Timer(new TimerCallback(CallbackMethod), 
                sName, 0, 1000);
            Console.WriteLine(
                "Naciśnij dowolny przycisk, aby zakończyć program.");
            Console.Read();
            oTimer.Dispose();
        }

        private void CallbackMethod(object oStateObject)
        {
            Console.WriteLine(
                "Upłyneło {0} milisekud odkąd tu jesteś z nami {1}.",
                Environment.TickCount - m_nStart, oStateObject.ToString());
        }
    }
}

Przykład powinien wyglądać dla Was dosyć swojsko:) W tym przypadku zastosowałem elementy charakterystyczne dla tego Timera (już na wstępie zwróćcie uwagę na inną przestrzeń nazw). Pojawiła się metoda CallbackMethod (32-37), w której również wypisujemy tekst użytkownikowi. Tym razem wyświetlamy mu jednak dodatkowo jego imię, które przekazaliśmy sobie obiektem stanu. Oczywiście tak jak wspomniałem wcześniej, można go wykorzystać w dużo bardziej zaawansowany sposób.

W metodzie StartTimer również pojawiły się nowe elementy. Na wstępie (tuż po ustawieniu zmiennej m_nStart) musimy pobrać imię aktualnego użytkownika. Obiekt Timera tworzymy w linii 24. Wątkowy Timer wymaga zastosowania klasy TimerCallback, której przekazujemy nazwę naszej wcześniej zdefiniowanej metody. Ustawiamy również okres od rozpoczęcia do uruchomienia na 0 milisekund(czyli natychmiast), a interwał pomiędzy kolejnymi wywołaniami na 1000.

Następnie oczekujemy na naciśnięcie przycisku przez użytkownika, a sam Timer zwalniamy dzięki metodzie Dispose.

Zwróćcie tutaj uwagę na brak metod Start i Stop. Uruchomienie Timera odbywa się zaraz po utworzeniu obiektu. Nie jest to jednak równoznaczne z rozpoczęciem realizacji powierzonych mu zadań. Te mogą zostać opóźnione przez parametr dueTime, o którym pisałem wcześniej. Jeśli ma on wartość 0 (tak jak w naszym przypadku) to praca rozpocznie się natychmiast.

Zamykanie Timera następuje poprzez wywołanie metody Dispose, która jest powszechnie używana do zwalniania zasobów wskazanych obiektów (możecie ją zaimplementować do własnych obiektów, korzystając z interfejsu IDisposable).

Podsumowanie

W dzisiejszym wpisie przedstawiłem Wam dwie klasy Timer, które ulokowane zostały w różnych przestrzeniach nazw. Jak widać, choć zastosowanie obu klas jest bardzo podobne, jednak implementacja oraz zachowanie różni się znacząco w kilku aspektach (odsyłam do akapitu System.Timers.Timer oraz System.Threading.Timer - różnice oraz samych przykładów). Bezpieczniej jest na pewno stosować pierwszą z klas, która jest domyślnie zabezpieczona przed wątkami (w przypadku tej drugiej, ten ciężar spoczywa na naszych barkach). W praktyce jednak docelowa implementacja będzie uzależniona od uwarunkowań określonej aplikacji.

Literatura

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus