Kod otwarty na zdarzenia

2010-08-13 18:35

Wydaje mi się, że przynajmniej pod kilkoma względami programowanie przypomina prowadzenie samochodu. Obu tych umiejętności względnie łatwo się nauczyć i niemal niemożliwe jest zapomnieć. Po nabyciu pewnej wprawy mamy też wystarczającą biegłość, by nie musieć koncentrować całej uwagi na którejś z tych czynności. Wyjątkiem są jedynie te miejsca, w których zalecane jest zachowanie szczególnej ostrożności.
Dla mnie (i pewnie nie tylko dla mnie) takimi miejscami w kodzie są styki programu z otoczeniem, przez które musi przebiegać jakaś komunikacja polegająca na wymianie danych i/lub informacji o zdarzeniach. Fragmenty te są ważne i nierzadko problematyczne, gdyż często pociągają za sobą konieczność dopasowania sposobu wykonywania programu do zewnętrznych wymagań. Jak na skrzyżowaniu, trzeba czasem chwilę poczekać i przynajmniej parę razy obejrzeć się dookoła.

W tym kontekście używa się słowa ‘synchronizacja’, jednak kojarzy mi się ono nieodparcie z programowaniem współbieżnym. To trochę złe skojarzenie, bowiem nie trzeba wcale tworzyć kilku wątków czy procesów, by kwestia zaczynała mieć znaczenie. Wystarczą operacje wejścia-wyjścia (zwłaszcza względem mediów wolniejszych niż lokalny system plików) lub obsługa jakiegoś rodzaju zdarzeń zewnętrznych, albo chociażby RPC (Remote Procedure Call – zdalne wywoływanie procedur) – ale oczywiście ta lista nie wyczerpuje wszystkich możliwości. Ogólnie chodzi o wszelkie odstępstwa od możliwości wykonywania programu krok po kroku – a właściwie kroczek za kroczkiem, gdzie każda sekwencja stałej liczby instrukcji zajmuje umowną, niezauważalną chwilę.

Jeśli by się nad tym zastanowić przez moment, to takich sytuacji jest sporo. Ba, właściwie to wspomniane scenariusze sekwencyjne są raczej wyjątkiem, a nie regułą. Dzisiejsze aplikacje działają w wielozadaniowych środowiskach, na ograniczonych pulach zasobów, wymieniają dane przez różne (niekoniecznie szybkie) kanały informacji, i jeszcze dodatkowo są pod ciągłą presją wymagań co do cechy określanej angielskim słówkiem responsiveness – czyli “komunikatywności z użytkownikiem”. Nic dziwnego, że wspomniane przeze mnie wcześniej ‘punkty szczególnej ostrożności’ stają się na tyle istotne, że zazwyczaj to wokół nich buduje się całą architekturę programu.
W jaki sposób są one realizowane? Zależy to od wielu czynników, w tym od rodzaju aplikacji oraz możliwości i struktury systemowego API, które ona wykorzystuje. Tym niemniej można wyróżnić kilka schematów, aplikowalnych w wielu sytuacjach – chociaż nie zawsze z równie dobrym skutkiem. Są nimi:

  • Aktywne czekanie (busy waiting, active polling), polegające na ciągłym dopytywaniu się o rezultat operacji czy też informację o tym, czy oczekiwane zdarzenie zaszło. Stosowane w praktyce sprowadza się do pętli podobnej do poniższej:
    1. while (!WaitFinished()) { }

    i jest przestępstwem ściganym z urzędu w każdym rozsądnym zespole projektowym :) Istnieją aczkolwiek okoliczności łagodzące, zezwalające na jego popełnienie pod ściśle określonymi warunkami. Musimy jedynie być pewni, że zużywanie do 100% czasu procesora przez nasz program jest akceptowalne i że między kolejnymi zapytaniami możemy też zrobić coś produktywnego. Tak się składa, że istnieje typ aplikacji, w którym oba te warunki mogą być spełnione: gry. Ich pętla główna to nic innego jak aktywne czekanie na informacje o zdarzeniach z renderowaniem kolejnych klatek w tak zwanym międzyczasie.

  • Na wpół aktywne czekanie (soft busy waiting) to złagodzona wersja powyższego. Nie zasypujemy już systemu żądaniami z prędkością światła, a jedynie co jakiś czas. Pomiędzy nimi program może zająć się swoimi sprawami albo po prostu… spać:
    1. while (!WaitFinished()) { Sleep(50); }

    Takie krótkie drzemki zdecydowanie zmniejszają zużycie procesora przez aplikację, ale nie dają jej dużego pola manewru. Ta metoda jest więc stosowalna główne dla usług działających w tle. A raczej byłaby, gdyby nie istniały znacznie lepsze :)

  • Przerywalne oczekiwanie (interruptible waiting) to stan zawieszenia w oczekiwaniu na zdarzenie (np. koniec długiej operacji), podczas którego możliwa jest jednak reakcja na pewne szczególne okoliczności. Koncepcja takiego oczekiwania jest zapewne dobrze znana programistom linuksowym, jako że tam niemal każda operacja może zostać przedwcześnie przerwana przyjściem sygnału do procesu. Ów sygnał może być złapany i w konsekwencji obsłużony, lecz nie powinno to pociągać za sobą żadnych skomplikowanych operacji.
    Przerywalne oczekiwanie jest akceptowalnym sposobem działania programów funkcjonujących w tle, na przykład jako usługi systemowe. Niespecjalnie za to nadaje się dla aplikacji z UI – chyba że co jakiś krótki czas pozwolimy na obsłużenie wejścia od użytkownika. Wtedy jednak jest to już raczej wariant soft busy waiting.
  • Asynchroniczne wywołanie zwrotne (asynchronous callback) znaczy mniej więcej tyle, co “zrób to i powiedz mi, kiedy skończysz” albo “powiadom mnie, gdy to się zdarzy”. W obu przypadkach musimy dostarczyć mechanizm wywołania zwrotnego (callback), zintegrowany z naszą aplikacją. To dosyć wygodne, jeśli i bez tego działa ona na zasadzie sterowania zdarzeniami (event-driven) i rzeczone wywołanie możemy potraktować jako jeszcze jedną sytuację do obsłużenia. (Dlatego komponent BackgroundWorker z Windows Forms jest tak prosty w użyciu). Gorzej jest wtedy, gdy na potrzeby asynchronicznego callbacku musimy rozbić na kilka części (i stanów) program, który bez tego działałby niemal sekwencyjnie.
    Osobną kwestią jest też sposób realizacji callbacka. W dużej mierze zależy on od możliwości języka. W jednych jest to bardzo wygodne (C#, Python, większość języków skryptowych), w innych nieco mniej (Java), a w jeszcze innych ledwo da się to zrobić (C++, a jakże).

Między powyższymi sposobami możliwe są “konwersje”, oczywiście do pewnego stopnia. Wymagać to może uruchomienia dodatkowego wątku, w którym wykonujemy polling operację asynchroniczną lub wręcz blokującą, i którego stan możemy odpytywać lub otrzymać jako sygnał na obiekcie synchronizacyjnym po wejściu w stan przerywalnego czekania.
Nieczęsto jednak taka zabawa ma uzasadnienie. W najlepszym razie otrzymamy rozwiązanie równoważne, a najgorszym stracimy na wydajności operacji dostosowanej pod konkretny typ powiadamiania. Lepiej jest jednak trzymać się tego, co dana platforma i API nam proponuje.

Tags: , , , ,
Author: Xion, posted under Programming »


4 comments for post “Kod otwarty na zdarzenia”.
  1. Anonymous:
    August 14th, 2010 o 18:25

    szczerze nie wiem po co się bawić w pisanie aplikacji okienkowych które nie muszą być nastawione na wysoka wyjdanosc (pod windows oczywiscie) w c++. Przecież java a w szczególności C# to jest poezja w porównaniu z winapi.

  2. Kiro:
    August 16th, 2010 o 10:37

    A to są jeszcze jacyś masochiści piszący w winapi coś więcej niż tworzenie okna do gry?

  3. dynax:
    August 18th, 2010 o 14:17

    Jeśli chodzi o obsługę sygnałów to poleca projekt Ace Framework (http://www.cs.wustl.edu/~schmidt/ACE.html). Połączenie systemowych signals ze standardem OOP.

  4. Anonymous:
    August 19th, 2010 o 16:40

    A to są jeszcze jacyś masochiści piszący w winapi coś więcej niż tworzenie okna do gry?

    Praktyczne każdy nowicjusz który zaczyna sie uczyć c++

Comments are disabled.
 


© 2018 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.