Podobno programowanie równoległe jest trudne: fakt, że kilka czynności może być wykonywanych “jednocześnie”, wprowadzać ma duże zamieszanie i komplikować życie. Nie zgadzam się! Nawet jeśli procesy lub wątki działają w tym samym czasie, to jeszcze nie jest powód do zmartwień. Prawdziwe problemy zaczynają się przecież dopiero wtedy, gdy trzeba ich działanie zsynchronizować :-)
A do tego przydają się na przykład semafory. Przy ich pomocy których można zresztą zrealizować większość popularnych mechanizmów blokowanej synchronizacji, jak choćby sekcje krytyczne. Jednak cała sztuka polega zawsze na tym, żeby użyć ich w sposób odpowiedni do danego zastosowania i nie nabawić się przy tym zagłodzenia, zakleszczenia (deadlock) lub innych “równoległych” błędów.
Istnieją oczywiście tzw. schematy synchronizacji, lecz ich przydatność jest mniej więcej kilkukrotnie mniejsza niż chociażby wzorców projektowych dla programowania obiektowego. Zauważyłem jednak, że sytuacje, w których używa się semaforów (lub podobnych obiektów) można zakwalifikować do jednej z trzech grup, z których każda następna jest ugólnieniem poprzedniej:
W ten sposób tworzy się oczywiście sekcja krytyczna, bo tylko jeden proces uzyska do naszego zasobu dostęp. Wygodne jest tutaj myślenie o semaforze jako obiekcie ochraniającym zasób, a nie fragment kodu. Oczywiście na samym początku taki semafor jest zwykle opuszczony.
Bo programowanie równoległe – którego synchronizacja jest nieodłączną częścią – to jednak ciężki kawałek chleba ;]
Jedną z bardziej denerwujących cech bibliotek, z jakimi można się zetknąć, jest ich wewnętrzna niespójność pod względem interfejsu. Dotyczy to na przykład drobnych kruczków związanych z postacią wywołań funkcji i metod. Znanym przedstawicielem tego gatunku “kwiatków” są chociażby standardowe funkcje C(++) od zapisu do plików – fprintf
i fputs
:
Obie działają podobnie (z dokładnością do formatowania w fprintf
), lecz różnią się irytującym szczegółem: miejscem parametru określającego plik (typ FILE*
), do którego zapisujemy. Nietrudno się tu pomylić i to niekoniecznie wtedy, gdy korzystamy z obu funkcji w tym samym kodzie.
Podobna niejednolitość jest na szczęście w miarę łatwa do wykrycia i usunięcia – oczywiście pod warunkiem, że mówimy tutaj o bibliotece, której nie wykorzystuje jeszcze nikt postronny. Oprócz niespójnych prototypów funkcji powinniśmy jeszcze zwrócić uwagę na:
Get/SetSomething
, IsEnabled
, itd. Podobnie metody z nazwami zawierającymi czasowniki “znaczące”, takie jak load, create, read, write powinny przede wszystkim robić to, co owe czasowniki wyrażają.Nanoszenie podobnych poprawek jest oczywiście dość skomplikowaną operacją, ale wciąż nie ingeruje ona ani w projekt biblioteki, ani – z grubsza – w sposób, w jaki końcowy programista ma jej używać. Mimo to podejrzewam, że w przypadku mojego siln… tj. kodu będzie to dosyć czasochłonne. Całkiem często zmieniałem bowiem swoje upodobania odnośnie kwestii, które wymieniłem powyżej (i których lista nie jest też pewnie kompletna).
Ale cóż, wiosna dopiero się zaczyna, więc czasu – przynajmniej teoretycznie – jest dużo ;-)
k_b wcielił w życie bardzo ciekawy pomysł, polegający na wydawaniu co jakiś czas magazynu opisującego wydarzenia dziejące się na Warsztacie. Chodzi tu na przykład o postępy w amatorskich projektach, które są z tym community związane, interesujące screeny, ciekawostki z forum i inne warte odnotowania fakty. W o wiele skromniejszej formie coś takiego już występowało w postaci tzw. Community News, ale ostatecznie niezbyt się to przyjęło.
Miejmy nadzieję, że z WMagiem – bo tak w skrócie się to wydawnictwo nazywa – będzie inaczej. Pierwszy numer nie grzeszy może zachwycającą szatą graficzną, ale i tak warto mu się przyjrzeć. Do czego zachęcam, prezentując poniższe linki:
Wprawdzie Google ostatecznie rozstrzyga rywalizację między Gwiazdką a Wielkanocą na korzyść tej pierwszej, ale to zwyczaj z okolic święta wiosennego użyczył nazwy dla pewnej zabawnej praktyki programistycznej. Chodzi o easter eggs, czyli ukryte napisy, ekrany, a czasem całe pod-aplikacje, obecne w programie acz niedostępne niewtajemniczonym użytkownikom. Mimo tego, wiele spośród tych ‘jaj’ stało się wyjątkowo znanych.
Dotyczy to naturalnie tych aplikacji, które są szeroko znane – czyli na przykład tych wyprodukowanych przez znaną i (nie)lubianą firmę z Redmond. Różne wersje pakietu Office zawierają przykładowo przynajmniej kilka różnych gier, począwszy od prostego flippera (Word 97) przez symulator lotu aż po… grę FPP (we wczesnej wersji Excela). W Windows poniżej wersji XP można było też wykorzystać ciekawostkę ukrytą w jednym z wygaszaczy ekranu, który mógł wyświetlać kolejno nazwy znanych wulkanów na świecie.
Spora ilość easter eggs jest zawarta w okienkach About programów – co jest całkiem zrozumiałe, zważywszy że prawie nikt tam nie zagląda :) Do bardziej znanych należy chyba odgłos wydawany po kliknięciu w nos na fotografii twórcy programu mIRC.
Jaja możemy jednak znaleźć nie tylko w programach użytkowych, ale także w grach. Kto nie zna słynnego “krowiego poziomu” (cow level) z Diablo II? Blizzard ma zresztą więcej takich kwiatków w zanadrzu – jak choćby zabawne dźwięki wydawane przez jednostki w obu znanych RTS-ach (WarCraft i StarCraft). W World of Warcraft ilość różnych smaczków liczy się co najmniej w tysiącach.
Niestety, przed easter eggs przyszłość rysuje się ponuro. Możliwość umieszczenia w kodzie programu nieudokumentowanego kodu jest bowiem potencjalnym zagrożeniem i może prowadzić do powstania tzw. bomb logicznych. To oczywiście zmartwienie koncernów, które mogą nie ufać swoim programistom. Amatorskich produkcji siłą rzeczy to nie dotyczy.
Tak więc umieszczajmy w swoich grach i aplikacjach jak najwięcej jajcarskich pomysłów, póki jeszcze możemy :) Wesołych świąt!
Przez ostatnich kilkanaście tygodni nie zaglądałem właściwie do kodu swojego siln… tzn. biblioteki (;P). I chociaż entropia kodu, którym nikt się nie zajmuje, nie powinna w zasadzie rosnąć, od czasu do czasu przydadzą się większe lub mniejsze porządki. Zwłaszcza jeśli w międzyczasie zmieniły się nasze poglądy dotyczące tego, jak programować trzeba, a jak nie należy.
Mniej więcej coś takiego przytrafiło mi się jakiś czas temu. Dość długo byłem zwolennikiem tzw. notacji węgierskiej. Jest to pewna reguła odnosząca się do nazw wprowadzanych do kodu źródłowego – takich jak nazwy zmiennych, klas, funkcji, itd. – która postuluje, aby poprzedzać je pewnymi przedrostkami. Prefiksy te mają dostarczać pewnych informacji, mówiąc na przykład, czy mamy do czynienia z klasą, strukturą czy ze zmienną. W tym ostatnim przypadku dodatkowo rozróżniamy też zmienne lokalne od pól klas, a także wprowadzamy przedrostki identyfikujące ich typy. Produktem tych zasad mogą być ostatecznie nazwy typu CFoo::m_pstrVar
.
Prawdopodobnie cała ta zabawa była przydatna wtedy, gdy nie dysponowało się zintegrowanymi środowiskami programistycznymi (IDE). Teraz nietrudno jest poznać typ zmiennej: wystarczy najechać kursorem na dowolną nazwę w kodzie, a w podpowiedzi dostaniemy jego deklarację. Proste i praktyczne.
Podejrzewam, że nadmierna sympatia do notacji węgierskiej wzięła się u mnie stąd, że stykałem się z nią dawno temu w kodach napisanych w C i C++, z których – nie ukrywajmy – niewiele wtedy rozumiałem :) Tym więc mogę usprawiedliwić fakt, że bezmyślnie przejąłem ten poroniony w gruncie rzeczy sposób nazewnictwa. Na szczęście nigdy nie jest za późno, aby nawrócić się na właściwą drogę, co też niniejszym czynię ;]
Wyrzucenie wszystkich prefiksów chociażby z nazw zmiennych byłoby sporym zadaniem, zwłaszcza że mówimy o kodzie liczącym przecież 30 tysięcy linii (co jest, przy okazji, bardzo dziwne, jeśli się weźmie pod uwagę jego mizerię funkcjonalną :)). Dlatego rozpoczynam skromnie: od poprawienia nazw wszystkich klas i struktur tak, aby nie były opatrzone żadnymi “ozdobnikami”. Nie ukrywam, że w ten sposób staram się upodobnić do konwencji stosowanej w .NET i JVM. Bo przecież dobre wzorce są po to, aby je naśladować…
Dawno, dawno temu miałem dość oryginalny pomysł na program użytkowy. Miała to być (o wiele) lepsza wersja konsoli Windows, której wyższość miała się objawiać w wyglądzie oraz możliwościach; pewne inspiracje czerpałem tutaj z terminali linuksowych oraz basha (nie, nie chodzi tu o serwis z cytatami z IRC-a ;P). Jak większość “genialnych” pomysłów, także i ten nie doczekał się realizacji. Przyczyną było to, iż nie za bardzo wówczas wiedziałem, jak można zapewnić, aby aplikacje konsolowe funkcjonowały w ramach projektowanego terminala tak, jak to robią w domyślnym oknie konsoli w Windows. Większość problemu rozbijała się o to, jak pobierać dane przez ów program produkowane i jak przekazywać do niego te wpisywane przez użytkownika.
Konsolka mała i ciasna, ale własna
Jako że było to bardzo dawno temu, nie znałem wtedy jeszcze takich terminów jak ‘standardowe wejście/wyjście’, ‘proces potomny’ czy ‘pipe‘, które byłyby tutaj bardzo pomocne. Dowiedziałem się o nich więcej dopiero znacznie później, przy czym wiedza ta w większości odnosiła się do systemu zgoła innego niż Windows :)
Pomyślałem jednak, że do problemu można by wrócić – zwłaszcza, że pytanie o to, jak uruchomić program z przekierowanym wejściem i wyjściem, pojawia się stosunkowo często. Okazało się, że nie jest to specjalnie trudne i sprowadza się właściwie do trzech kroków – z których tylko pierwszy może być nieco zakręcony. Cała procedura (w Windows) może wyglądać na przykład tak:
CreatePipe
), które posłużą nam do odbierania wyjścia od i dostarczania wejścia do procesu potomnego. Należy przy tym zwrócić uwagę na dwie rzeczy:
SECURITY_ATTRIBUTES
(tak, tej którą w 95% przypadków się ignoruje) i przekazując ją do funkcji tworzącej pipe.CreateProcess
). Musimy mu podać właściwe końce rurek w strukturze STARTUPINFO
oraz poinstruować Windows, by były one wykorzystywane (flaga STARTF_USESTDHANDLES
). Ponadto musimy wskazać, że chcemy dziedziczyć uchwyty i że utworzony proces konsolowy nie powinien pokazywać okienka (CREATE_NO_WINDOW
) – to już przekazujemy w parametrach CreateProcess
.WaitForMultipleObjects
. Natomiast organizacja wejścia zależy już od naszej aplikacji. Warto pamiętać, że w standardowej konsoli jest ono buforowane wierszami, zatem i my powinniśmy wysyłać coś do procesu potomnego dopiero wtedy, gdy zbierzemy całą linijkę danych wejściowych.Łatwe, prawda? ;-) W innych systemach operacyjnych robi się to minimalnie inaczej (pipe
, fork
, dup2
, …), ale ogólna idea jest podobna. Jak widać, kilkoma małymi rurkami można zdziałać całkiem sporo :]
Kod skompilowany nie musi być kodem działającym poprawnie – tę prawidłowość zna każdy programista. Z drugiej jednak strony dogłębne przetestowanie programu jako całości to proces żmudny, trudny, niepokojący i obarczony sporym ryzykiem niepowodzenia. A przy tym oczywiście konieczny. Można go jednak nieco usprawnić poprzez ograniczenie możliwości występowania błędów w niektórych częściach programu – takich, które wcześniej potraktujemy testami jednostkowymi (unit tests).
Ten rodzaj testów w skrócie polega na tym, aby każdy napisany element kodu – co zależnie od języka i sposobu programowania może oznaczać np. funkcję lub klasę – opatrzyć jedną lub kilkoma procedurami testującymi. Mają one na celu sprawdzanie, czy ów element robi dokładnie to, co założyliśmy, poprzez użycie go “na zewnątrz”, w innym kodzie. Innymi słowy, taka procedura testowa ma na celu uruchomienie funkcjonalności jakiejś klasy/funkcji/itp. i sprawdzenie, czy rezultaty jej działania są zgodne z oczekiwanymi.
Takie procedury można pisać po zakończeniu implementacji testowanego elementu. Niektóre metodyki tworzenia oprogramowania zalecają jednak, aby… najpierw pisać kod testowy, a dopiero potem ten właściwy! Wbrew pozorom ma to całkiem spory sens. Jeśli bowiem zaczniemy od kodu, który ma naszej klasy używać, to istnieje większa szansa, że od początku wyposażymy ją w interfejs, którego rzeczywiście da się używać bez większych problemów. Na koniec zaś będziemy dysponowali też przykładowym kodem, obrazującym sposób korzystania z danej klasy, co zazwyczaj bywa przydatne.
Według mnie największą zaletą testów jednostkowych jest to, że przy pomocy odpowiednich narzędzi możemy je bardzo szybko napisać w postaci nadającej się do natychmiastowego uruchomienia. Brak konieczności tworzenia nowej aplikacji – dla samych tylko testów – jest niezwykle wygodny. Najczęściej bowiem jedyne, co musimy zrobić, to napisać klasę z procedurami testowymi i oznaczyć ją odpowiednio w kodzie, a następnie po prostu uruchomić testy (co często daje się zautomatyzować jako czynność następną po kompilacji) i obserwować wyniki.
Szczegóły mogą się aczkolwiek różnić w zależności od języka, jako że frameworków dla testów jednostkowych jest sporo. Mamy na przykład:
Tak naprawdę to właściwie dla każdego liczącego się języka istnieje narzędzie tego typu. Polecam więc przyjrzenie się im (i całemu zagadnieniu testów jednostkowych), jeśli wcześniej nie mieliśmy z tym tematem styczności. Bo przecież testowania nigdy dość ;)