Posts tagged ‘I/O’

Obsługa strumienia w Javie

2011-07-26 20:37

W językach kompilowanych mechanizmy wejścia-wyjścia opiera się niemal zawsze o koncepcję strumienia (stream), czyli abstrakcyjnego obiektu z którego możemy czytać dane i/lub je do niego zapisywać. Ponieważ strumień jest opakowaniem na jakiś zewnętrzny zasób – plik, połączenie sieciowe, itp. – należy generalnie dbać o jego poprawne i szybkie zamknięcie, gdy nie jest już potrzebny. Dotyczy to także zwłaszcza języków z zarządzaną pamięcią, gdzie osierocony obiekt strumienia może nie być posprzątany przez bardzo długi czas, zajmując zewnętrzne, niezarządzane zasoby systemowe.

Poprawny sposób postępowania z obiektem strumienia, który chciałem dzisiaj omówić, dotyczy konkretnie języka Java, gdyż tam cała sprawa jest co najmniej nietrywialna. Dzieje się tak z trzech powodów:

  • W Javie każdy wyjątek musi zostać albo złapany (catch), albo zadeklarowany jako opuszczający funkcję (throws). Jest to sprawdzane podczas kompilacji.
  • Metoda close, zamykająca strumień, deklaruje potencjalne wyrzucanie wyjątku IOException.
  • Java poniżej wersji 7 nie posiada odpowiednika konstrukcji with (obecnej np. w C# i Pythonie), która automatycznie posprzątałaby po obiekcie strumienia w momencie opuszczenia jej zasięgu.

Brak instrukcji with sprawia, iż do dyspozycji pozostaje nam wyłącznie try-catch lub try-finally. Naiwne zastosowanie któregoś z nich nie daje jednak pożądanych efektów:

  1. try {
  2.     InputStream is = new FileInputStream("file.txt");
  3.     // ...
  4. } finally {
  5.     is.close(); // ups!
  6. }

Takim efektem byłby na przykład fakt kompilowania się kodu :) W tej wersji jest to jednak niemożliwe (o ile funkcja nie deklaruje wyrzucania IOException), bowiem wyjątek ten może zostać rzucony przez metodę close… A przynajmniej taka jest teoria, którą kompilator niestety pedantycznie sprawdza.

W rzeczywistości ten kod ma przynajmniej jeszcze jeden błąd, którego nie wyeliminuje otoczenie wywołania close odpowiednim blokiem try-catch. Jego znalezienie pozostawiam aczkolwiek jako – ahem – ćwiczenie dla czytelnika ;) W zamian pokażę dla odmiany nieco lepszy sposób na obejście zaprezentowanych problemów.
Polega on na zastosowaniu dwóch zagnieżdżonych bloków try: jednego z catch do złapania IOException i drugiego z finally do zamknięcia strumienia. W całości prezentuje się to następująco:

  1. try {
  2.     InputStream is = new FileInputStream("file.txt");
  3.     try {
  4.         // (czytamy ze strumienia)
  5.     } finally {
  6.         is.close();
  7.     }
  8. } catch (IOException e) {
  9.     // ...
  10. }

Przy zastosowaniu takiej konstrukcji wszystkie miejsca, w których wyjątek I/O może wystąpić, są otoczone blokiem try-catch, więc kompilator nie będzie miał powodów do narzekań. Nadal też gwarantujemy, że strumień zostanie zawsze zamknięty, co z kolei zapewnia blok try-finally.

A że wygląda to wszystko cokolwiek nieestetycznie? Cóż… Java :)

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

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

Grubo zapakowane I/O

2007-11-06 12:02

W każdym języku programowania potrzebny jest system wejścia/wyjścia. To zresztą bardzo często wykorzystywana jego część, więc powinna charakteryzować się wysoką jakością. Chcielibyśmy mianowicie nie tylko tego, aby I/O było szybkie. Powinno być też elastyczne i proste w obsłudze. Czasami udaje się te wymagania pogodzić w całkiem zadowalający rezultat, a czasem nie.

Weźmy na przykład Javę. Posiada ona bardzo rozwinięty system wejścia/wyjścia, umożliwiający odczyt i zapis z wielu różnych źródeł: ekranu, plików, gniazdek sieciowych, potoków łączacych wątki, itp. Ponadto komunikacja może odbywać się na wiele sposobów: mamy na przykład dość “surowe” strumienie, nieco bardziej użyteczne czytacze (readers) i zapisywacze (writers), a także kanały (channel) i bufory (buffers).
Cały system wydaje się zatem bardzo zaawansowany. Niestety, w praktyce jest on zdecydowanie przerośnięty, a poza tym charakteryzuje się pewną ‘ciekawą’ cechą – nazwijmy to – projektową. Osobiście uważam, że twórcy Javy w tym momencie przedobrzyli i chcąc zastosować bardzo elastyczny w założeniu wzorzec Dekorator, stworzyli interfejsowy koszmarek. Wspomniany wzorzec polega na kolejnym “opakowywaniu” obiektów tak, aby rozszerzać ich możliwości; obiekt ‘zewnętrzny’ nie musi przy tym wiedzieć dokładnie, czym jest obiekt ‘wewnętrzny’. I tutaj rzeczywiście tak jest, lecz na nieszczęscie sami musimy zawinąć obiekt w te wszystkie warstwy.
Przykład? System.in, czyli strumień standardowego wejścia, w swej pierwotnej postaci jest niemal zupełnie bezużyteczny. Żeby zrobić z nim cokolwiek sensownego (np. odczytać linię tekstu), musimy najpierw opakować go do postaci odpowiedniego czytacza:

Podobnie jest chociażby z plikami. Za każdym razem musimy ubrać nasz obiekt na cebulkę, aby był on przydatny, przechodząc przy okazji przez cały arsenał bardzo różnych klas, od których jest wręcz gęsto w JDK.
Trzeba aczkolwiek przyznać uczciwie, że System.IO z .NET też zawiera całe mnóstwo różnych klas. Tam konieczność podobnego opakowywania zachodzi jednak o wiele rzadziej, gdyż interfejsy tych klas są trochę inteligentniejsze.

A co ze “staroświeckimi” językami, jak C++ czy Delphi? No cóż, w nich operuje się głównie pojęciem uniwersalnych strumieni i w zasadzie niczego więcej. Nie trzeba ich jednak niczym otaczać, bo fabrycznie potrafią już chociażby operować na podstawowych typach danych, a nie tylko ciągach bitów. Niby to mniej elastyczne i nie tak “obiektowo czyste”, ale o ile przyjemniejsze w użyciu.

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


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