Deweloperzy programujący wielowątkowo zapewne znają klasyczne typy wykorzystywanych przy okazji obiektów. Są to na przykład semafory, sekcje krytyczne (zwane też semaforami binarnymi) czy zdarzenia (events). Wszystkie one służą oczywiście do synchronizacji wątków tak, aby wykluczyć jednoczesny, wykluczający się dostęp do jednego zasobu.
Tego typu obiekty są wykorzystywane jednak głównie wtedy, kiedy mechanizm wątków jest zrealizowany w sposób specyficzny dla systemu operacyjnego – jak choćby poprzez API z Windows lub bibliotekę pthreads z Linuxa. Jeśli jednak mamy szczęście pracować z językiem, którego wielowątkowość jest częścią, wówczas korzysta się zwykle z nieco innych technik.
Taka sytuacja jest na przykład w Javie. Tam każdy obiekt (czyli instancja klasy dziedziczącej z java.lang.Object
) może być użyty jako obiekt synchronizujący. Z grubsza działa to w ten sposób, że gdy jeden z wątków zadeklaruje wykorzystanie danego obiektu – przy pomocy słowa kluczowego synchronized
– pozostałe nie mogą zrobić tego samego. Taka synchronizacja może odbywać się na wiele (składniowych) sposobów, jak choćby zadeklarowanie całej metody jak synchronizowanej:
W tym prościutkim przykładzie mamy zagwarantowane, że żaden postronny wątek nie wtrąci się w operację inkrementacji ze zwróceniem wartości (która nie jest atomowa) i stan licznika będzie zawsze poprawny.
Tak więc mamy semafory tudzież sekcje krytyczne. A co np. ze zdarzeniami (sygnałami)? Otóż każdy obiekt posiada metody wait
i notify
, umożliwiające czekanie na powiadomienie z innego wątku i oczywiście wysłanie takiego powiadomienia. Całkiem skuteczne i dosyć proste; naturalnie na tyle, na ile proste może być programowanie wielowątkowe :)
Ale czy oryginalne? Otóż dziwnym trafem na platformie .NET cała sprawa wygląda niemal dokładnie tak samo :) Odwzorowania przytoczonych elementów Javy w C# to odpowiednio: lock
(z dokładnością do kilku niuansów), Monitor.Wait
i Monitor.Pulse
. Sam sposób tworzenia wątków jest zresztą też bardzo bardzo podobny.
Wszelka zbieżność przypadkowa? Zdecydowanie nie. Lecz dobre rozwiązania warto jest przecież rozpowszechniać :]
Protokół HTTP i World Wide Web powstały już bardzo dawno temu, chociaż od tamtego czasu doczekały się całego mnóstwa usprawnień. Pewnie największym była możliwość generowania stron HTML dynamicznie przy użyciu skryptów CGI, PHP, i tak dalej. Dzięki temu strony umieją już całkiem sporo i nie są statycznymi dokumentami. Ich tworzenie zaczęło też bardziej przypominać pisanie normalnych aplikacji.
Pewne ograniczenia HTTP ciężko jest jednak przezwyciężyć. Największym jest chyba to, że serwery każde zgłoszenie (request), które do nich napływa, traktują zupełnie odrębnie i w oderwaniu od wszystkich innych. Działają więc na zasadzie pytanie-odpowiedź. Gdyby poszukać odpowiednika tego modelu w zwyczajnych programach, to byłyby nim narzędzia konsolowe obsługiwane w całości przełącznikami wiersza poleceń. W dobie interfejsów graficznych mieniących się wszystkimi kolorami palety RGB nie wyglądają one zbyt imponująco…
Dlatego też wszelkie platformy służące uruchamianiu aplikacji webowych starają się tę niedogodność jakoś obejść i umożliwić powiązanie poszczególnych żądań oraz przechowywanie danych pomiędzy nimi. Standardem jest mechanizm sesji, ale np. w PHP nigdy nie udało mi się do końca go zrozumieć :)
Ostatnio jednak z konieczności (nietrudno się domyślić, jakiej :)) zajmuję się ASP.NET i muszę przyznać, że tam cała sprawa została potraktowana nieporównywalnie lepiej. Przede wszystkim serwis jest tam rzeczywiście traktowany jako aplikacja, której instancja uruchamia się w momencie pierwszego requestu z danego hosta. Poszczególne strony przypominają też raczej okienka zwykłych programów, na których zamieszcza się kontrolki specyficzne dla ASP (przerabiane na wyjściu na HTML). I to ze wszystkimi konsekwencjami: one rzeczywiście istnieją na serwerze. To sprawia, że możemy np. pobrać dane formularza wypełnionego przed chwilą na poprzedniej stronie, po prostu czytając zawartość jej kontrolek.
Cała ta trwałość nie jest oczywiście aż tak daleko posunięta jak w tradycyjnych programach. Zdarza się (i to dość często), że musimy zwracać uwagę na to, kiedy strona może być przeładowywana. Ale i tak stanowi to spory postęp na przykład w stosunku do wspomnianego mechanizmu sesji (który naturalnie też jest dostępny).
Naturalnie wszystko to są jednak tylko obejścia do starego protokołu, który pierwotnie nie został zaprojektowany do tak złożonych zadań, do których się go obecnie wykorzystuje.
Koderzy wyróżniają się ponadprzeciętnym stopniem używania klawiatury, a pewnie i myszki. Miliony wciśnięć klawiszy, tysiące kliknięć i kilometry przejechane kursorem… Ale ile jest ich dokładnie? Można to policzyć jednym z wielu dostępnych programików. Samodzielnie napisanie takiej aplikacji też nie jest bardzo trudne: w Windows na przykład trzeba by użyć do tego mechanizmu hooków.
Jak każda zabawa, także i ta staje się jednak o wiele atrakcyjniejsza, jeśli w grę zaczyna wchodzić Internet :) Kiedyś funkcjonowało przedsięwzięcie znane jako Project Dolphin, którego celem było zliczanie wciśnięć klawiszy swoich użytkowników. Po rejestracji wystarczyło ściągnąć niewielki program klienta, działający w tle i wytrwale uaktualniający licznik stuknięć w klawiaturę. Co jakiś czas dane te były wysyłane na serwer.
Na identycznej zasadzie działa serwis WhatPulse. Posiada on aczkolwiek nieco większe możliwości, jako że statystyki dotyczą też aktywności myszy, a ponadto umożliwia zakładanie osobnych profili dla różnych komputerów, na których pracujemy. Ogólna zasada jest jednak taka sama.
Co daje uczestnictwo w takim projekcie – naturalnie oprócz poświęcenia małej części zasobów systemowych? Oczywiście dozgonną satysfakcję i możliwość śledzenia rosnących numerków :) Można rzecz jasna wymyślać bardziej racjonalne wyjaśnienia, jak na przykład mierzenie wydajności programistycznej wskazywanej przez liczbę kilobajtów wklepanego tekstu. Ale ponieważ zliczane są absolutne wszystkie wciśnięcia (i zresztą nic więcej – nie jest to żaden keylogger), równie dobry wynik osiągnie i superprogramista, i nałogowy użytkownik IRC-a.
Najlepiej więc jest być naraz i jednym, i drugim :) Na pewno zaś nie zaszkodzi przyjrzeć się generowanym przez siebie statystkom. Mogą być całkiem dobrą, dodatkową motywacją do… pracy ;)
Teoretycznie wszystko zakodować można “na żywioł”, po prostu siadając do ulubionego środowiska programistycznego, pisząc i kompilując. Proste dzieła może i można w ten sposób stworzyć, ale do czegoś bardziej skomplikowanego zawsze pomaga chociaż pobieżny projekt.
W jakiej formie? To już nie jest sprawą taką prostą. Obecnie mamy sporo różnych narzędzi, umożliwiających formowanie naszych pomysłów w miejscu ich docelowego egzystencji – czyli programów komputerowych. I nie chodzi mi tu o aplikacje wysoce wyspecjalizowane, bo do zaprojektowania programu może się okazać przydatny edytor tekstu typu Notatnika. Każde z tych narzędzi będzie użyteczne dla określonego celu i każde ma też swoje ograniczenia.
Ale mamy również inny sposób. Możemy odejść (albo przynajmniej się odwrócić) od komputera, wziąć kartkę, długopis i… do dzieła. Być może wkrótce posługiwanie się tak “archaicznymi” sprzętami będzie nieco staroświeckie, ale pewnie jeszcze długo będzie miało niezaprzeczalne zalety:
Każdy jednak wie rzecz jasna, jakie są jego wady. Niektóre alternatywy – jak na przykład skrobanie po tabletowym komputerze – niwelują część z nich, zbijając przy okazji niektóre zalety. Największym mankamentem jest chyba jednak to, że rysunkowe bazgroły na papierze zawsze już pozostaną bazgrołami: nie da się ich przetworzyć na użyteczną formę schematów czy tabel zrozumiałą dla komputera, a i rozpoznawanie czystego tekstu też nie jest jeszcze doskonałe.
Wygląda więc na to, że poczciwa kartka papieru jeszcze długo będzie podstawowym środkiem tworzenia przynajmniej wstępnych – nomen omen – szkiców, zostawiając bardziej szczegółowe projektowanie innym narzędziom. Warto zatem wciąż kultywować starożytną umiejętność ręcznego pisania i rysowania :)
Kiedy już przekonamy się o korzyściach płynących z regularnego stosowania debuggera (a każdy początkujący programista powinien przekonać się o nich jak najszybciej), być może zaczniemy się zastanawiać, jakaż to “magia” sprawia, że cały ten proces w ogóle jest możliwy. W jaki sposób program śledzący potrafi dostać się do wnętrza naszej aplikacji i wylistować zawartość wszystkich zmiennych, nie mówiąc już o możliwości ustawiania punktów przerwań (breakpoints) czy pracy krokowej. Czy debuggery korzystają z jakichś nieudokumentowanych “wytrychów” do samego jądra systemu operacyjnego?…
Otóż niekoniecznie. Na przykład Windows API udostępnia zwyczajne funkcje systemowe, za pomocą których jeden proces może śledzić zachowanie innego. Obejmuje to przyłączenie się do innego procesu w charakterze debuggera, obsługę przeróżnych interesujących zdarzeń (jak np. załadowanie biblioteki DLL lub stworzenie nowego wątku przez proces, który śledzimy), a także odczytywanie zawartości pamięci procesu (jeśli mamy do tego uprawnienia) lub kontekstu jego wątku.
Naturalnie mało kto będzie pisał swój własny debugger, więc wspomniane funkcje są nieszczególnie przydatne dla większości programistów. Jednak istnieje też kilka takich, które można użytecznie stosować po drugiej stronie – w aplikacji, która jest debugowana:
OutputDebugString
to prawdopodobnie ta najbardziej znana. Służy ona do wysyłania komunikatów do debuggera, które zwykle są wyświetlane w specjalnym okienku naszego IDE. Zazwyczaj jest to bardziej poręczne rozwiązanie niż chociażby pokazywanie okienek komunikatów, które trzeba przecież potwierdzać kliknięciami w OK. Funkcję tę można aczkolwiek zastąpić wypisywaniem na standardowe wyjście diagnostyczne (stderr
, reprezentowane w iostream przez obiekty cerr
lub wcerr
).DebugBreak
działa z kolei jak punkt przerwania. Zasadniczo funkcja ta wywołuje odpowiedni wyjątek systemowy (nie mający za wiele wspólnego z wyjątkami języka C++), który każdy porządny debugger złapie. W środowisku programistycznym rezultatem będzie zawieszenie działania programu i ustawienie się z widokiem kodu źródłowego na miejsce wywołania funkcji, co oczywiście znakomicie ułatwia odnalezienie przyczyny błędu. Jeżeli jednak nikt nie śledzi naszej aplikacji, wówczas rzucony wyjątek spowoduje jej awaryjne zakończenie. Generalnie podobny efekt co DebugBreak
powinno dać wywołanie przerwania numer 3 (czyli asm { int 3 }
).IsDebuggerPresent
pozwala z kolei określić, czy aplikacja jest aktualnie debugowana przez jakiś inny proces. Dzięki temu możemy na przykład przekazywać do komunikatów diagnostycznych większą liczbę informacji, wiedząc, że ktoś “po drugiej stronie” faktycznie je odczytuje :)Wprawdzie samo korzystanie z tych funkcji nie zastąpi dobrego systemu kontroli błędów i ich raportowania (np. zapisywania w dzienniku), lecz z pewnością może pomóc. Bardzo dobrze prezentuje się na przykład okienko z obszernym komunikatem o błędzie i trzema możliwościami decyzji: kontynuacji programu, przejścia do trybu debugowania albo awaryjnego zakończenia aplikacji. Przy takim rozwiązaniu aż chce się popełniać błędy ;)
Grać każdy może – trochę lepiej lub trochę gorzej :) Wśród gier komputerowych każdy gatunek ma pozycje wybitne, które pierwsze przychodzą na myśl, kiedy tylko o nim wspomnimy. Przy każdej z nich można przesiedzieć długie godziny i dni, a inne gry często bywają nazywane “podobnymi do…” – Starcrafta, Quake’a, SimCity, Baldur’s Gate, i tak dalej.
Ale jest też pewien szczególny gatunek gier, mających już na starcie uprzywilejowaną pozycję. Należące do niego tytuły nie zawsze muszą odznaczać się wyjątkową oprawą graficzną, nowatorskim gameplayem, wieloma możliwymi sposobami prowadzenia rozgrywki – a mimo to często zdobywają rzesze graczy, którzy są im wierni przez całe miesiące, a nawet lata. Pewnie nietrudno zgadnąć, jaki gatunek gier mam na myśli. Chodzi mi bowiem o te określane akronimem MMORPG – Massive Multiplayer Online Roleplaying Game.
Zastanawiałem się niedawno, jaka jest tego przyczyna. W końcu mam pewne doświadczenia z kilkoma grami tego typu (a zwłaszcza z jedną :)), więc miałem nadzieję dojść do jakichś sensownych wniosków. W końcu wymyśliłem trzy powody, które wydają mi się najważniejsze.
Są to:
Nie zdziwiłbym się naturalnie, gdyby powyższa lista okazała się o wiele za krótka. Przeciwnie, byłoby to dość zaskakujące, jeśli fenomen gier MMORPG dało się zanalizować w tak trywialny sposób. Nadal też nie rozwiązałem dylematu, czy nad grami tego typu pożyteczniej jest się zastanawiać, czy może w nie… grać. Jak na razie obie te czynności wydają mi się marnowaniem cennego czasu ;D
W większości języków możemy zdefiniować nową nazwę dla istniejącego typu danych; nazywa się ją zwykle aliasem. I tak na przykład w C/C++ jest to możliwe za pomocą słowa kluczowego typedef
. Analogicznie w Delphi mamy od tego słowo kluczowe type
:
[delphi]type TMyInt = Integer;[/delphi]
Tak powstały typ TMyInt
jest faktycznie tylko aliasem. Zmienne należące do tego typu są bowiem całkowicie kompatybilne ze zmiennymi zwykłego typu całkowitego Integer
. W razie potrzeby konwersja między nimi może bez problemu zachodzić w obie strony.
Jeżeli jednak użylibyśmy deklaracji w formie:
[delphi]type TMyInt = type Integer;[/delphi]
wówczas TMyInt
byłby już zupełnie innym typem niż Integer
. Mimo że oba mogłyby przechowywać wartości tego samego rodzaju (liczby całkowite) i z tego samego zakresu, konwersje między nimi wymagałyby rzutowania.
Można by sądzić, że tworzenie takich typów rozróżnialnych “na siłę” jest bezcelowe. Zauważmy jednak, że typy wyliczeniowe (deklarowane przez enum
) są przecież także w gruncie rzeczy liczbami z określonego zbioru. Najczęściej jednak nie chcemy, aby możliwa była niejawna konwersja między nimi a normalnymi typami liczbami. Wszystko dlatego, że w enumach liczby nie pełnią funkcji liczb, tylko identyfikatorów pewnych stanów.
Podobnie tutaj w przypadku TMyInt
nie chodzi nam zapewne o liczby w sensie ich wartości, tylko o coś w rodzaju uchwytów – identyfikatorów obiektow. Kopalnią typów przeznaczonych do takiego właśnie celu jest oczywiście Windows API, zawierające tak znane i lubiane typy jak HWND
, HINSTANCE
, HDC
, itd. Wszystkie one są w gruncie rzeczy liczbami 32-bitowymi, a mimo to nie są ze sobą kompatybilne. Gdyby API to było obiektowe, obiekty reprezentowane obecnie przez te uchwyty należałyby do różnych klas.
Efekt niezgodności uchwytów osiągnięto, deklarując ich typy nie jako aliasy na DWORD
:
lecz jako niezwiązane ze sobą typy wskaźnikowe:
Można to uznać za dość pokrętną sztuczkę, ale na pewno jest ona lepsza niż tworzenie typu wyliczeniowego zawierającego nieco ponad 4 miliardy (232) nazwanych stałych ;]