Pula w¹tków

Transkrypt

Pula w¹tków
Rozdzia³ 11
Pula w¹tków
Czêœæ II: Jak to dzia³a
Rozdzia³ 11: Pula w¹tków
W rozdziale 8 mówiliœmy o tym, jak synchronizowaæ w¹tki za pomoc¹ mechanizmów umo¿liwiaj¹cych pozostanie w trybie u¿ytkownika. Wielk¹ zalet¹ tego typu synchronizacji jest jej szybkoœæ. Jeœli zale¿y ci na wydajnoœci w¹tków, powinieneœ zawsze sprawdziæ na pocz¹tku, czy synchronizacja w trybie u¿ytkownika nie jest ca³kowicie wystarczaj¹ca do twoich celów.
Wiesz ju¿, ¿e tworzenie wielow¹tkowych aplikacji jest trudne. Wi¹¿¹ siê z tym
dwa powa¿ne problemy: kontrolowanie tworzenia i usuwania w¹tków oraz synchronizowanie ich dostêpu do zasobów. Jeœli chodzi o ten drugi problem, system
Windows oferuje wiele narzêdzi wspomagaj¹cych, takich jak zdarzenia, semafory, muteksy, sekcje krytyczne i tak dalej. Korzystanie z nich jest doœæ ³atwe. Jedne,
co mog³oby z nimi konkurowaæ, to automatyczne zabezpieczanie dzielonych zasobów przez system. Zanim jednak Windows bêdzie oferowa³ tego typu ochronê,
mamy coœ, co powinno wszystkich zadowoliæ.
Ka¿dy z nas ma w³asne zdanie o tym, jak zarz¹dzaæ tworzeniem i usuwaniem
w¹tków. W ci¹gu ostatnich lat sam napisa³em kilka ró¿nych implementacji puli
w¹tków, z których ka¿da by³a œciœle dostosowana do konkretnej sytuacji. Microsoft Windows 2000 oferuje kilka nowych funkcji puli w¹tków, które u³atwiaj¹
tworzenie, usuwanie i korzystanie z w¹tków. Ta nowa uniwersalna pula w¹tków
z pewnoœci¹ nie nadaje siê do ka¿dej sytuacji, ale czêsto okazuje siê wystarczaj¹ca
i pozwala zaoszczêdziæ wiele godzin programowania.
Nowe funkcje puli w¹tków umo¿liwiaj¹ nastêpuj¹ce dzia³ania:
·
·
·
·
Asynchroniczne wywo³ywanie funkcji.
Wywo³ywanie funkcji w okreœlonych odstêpach czasu.
Wywo³ywanie funkcji po zasygnalizowaniu pojedynczego obiektu j¹dra.
Wywo³ywanie funkcji po zakoñczeniu asynchronicznych ¿¹dañ I/O.
Aby móc realizowaæ te zadania, pula w¹tków sk³ada siê z czterech oddzielnych
sk³adników. Sk³adniki te oraz rz¹dz¹ce nimi regu³y s¹ opisane w tabeli 11-1.
344
Czêœæ II: Jak to dzia³a
Tabela 11-1. Sk³adniki puli w¹tków i ich zachowanie
Zegar
Pocz¹tkowa
Zawsze 1
liczba w¹tków
Kiedy w¹tek
W momencie
jest tworzony wywo³ania
pierwszej
funkcji
zegarowej
puli w¹tków
Sk³adnik
Czekanie
I/O
Nie-I/O
1
0
0
Jeden w¹tek co
63 zarejestrowane
obiekty
System wykorzystuje metody
heurystyczne, ale jest kilka
czynników, które wp³ywaj¹ na
tworzenie w¹tków:
w Od dodania w¹tku min¹³ ju¿
pewien czas (w sekundach)
w Zosta³a u¿yta flaga
WT_EXECUTELONGFUNCTION
w D³ugoœæ kolejki przekroczy³a
pewien limit
Gdy w¹tek
Gdy w¹tek jest
bezczynny przez
obs³u¿y³
okreœlony czas
wszystkie
¿¹dania I/O
(oko³o minuty)
i jest bezczynny
przez okreœlony
czas (oko³o
minuty)
Kiedy w¹tek
jest usuwany
Podczas
zamykania
procesu
Gdy liczba
zarejestrowanych
obiektów
czekaj¹cych
wynosi 0
Jak czeka
w¹tek
W stanie
pogotowia
WaitForMultipleObjectsEx
Co budzi
w¹tek
Zasygnalizo- Zasygnalizowanie
wanie
obiektu j¹dra
czasomierza
przez ¿¹danie
u¿ytkownika
w kolejce APC
W stanie
pogotowia
GetQueued
CompletionStatus
APC
u¿ytkownika
w kolejce lub
zakoñczenie
¿¹dania I/O
Zg³oszenie statusu
zakoñczenia
lub zakoñczenie
¿¹dania I/O (port
zakoñczenia nie
pozwala, aby
jednoczeœnie
dzia³a³o wiêcej
w¹tków ni¿ 2 *
liczba CPU)
Podczas swojej inicjalizacji proces nie wymaga ¿adnych dodatkowych nak³adów
zwi¹zanych z tymi sk³adnikami. Ale gdy tylko zostanie wywo³ana jedna z nowych funkcji puli w¹tków, zostan¹ utworzone niektóre z tych sk³adników i czêœæ
z nich pozostanie ju¿ a¿ do zakoñczenia ca³ego procesu. Jak widzisz, koszty u¿ywania puli w¹tków nie s¹ trywialne – proces powiêksza siê o kilka dodatkowych
w¹tków i wewnêtrznych struktur danych. Dlatego musisz powa¿nie siê zastano-
Rozdzia³ 11: Pula w¹tków
345
wiæ, jakie korzyœci przyniesie ci pula w¹tków, a jakich nie przyniesie. Nie podejmuj decyzji „na œlepo”.
No dobrze, wystarczy tych ostrze¿eñ. Zobaczmy, jak to wygl¹da w praktyce.
Scenariusz 1: Asynchroniczne wywo³ywanie funkcji
Powiedzmy, ¿e masz proces serwerowy z g³ównym w¹tkiem czekaj¹cym na
¿¹dania klientów. Po otrzymaniu takiego ¿¹dania w celu jego obs³ugi zostaje
uruchomiony oddzielny w¹tek podrzêdny. Dziêki temu g³ówny w¹tek aplikacji
mo¿e czekaæ w pêtli na nastêpne ¿¹danie od klienta. Taki scenariusz jest typowy
dla aplikacji typu klient/serwer. Powinieneœ ju¿ wiedzieæ, jak mo¿na to zaimplementowaæ, ale mo¿esz te¿ skorzystaæ z nowych funkcji puli w¹tków.
Po otrzymaniu ¿¹dania klienta g³ówny w¹tek procesu serwerowego mo¿e wywo³aæ nastêpuj¹c¹ funkcjê:
BOOL QueueUserWorkItem(
PTHREAD_START_ROUTINE pfnCallback,
PVOID pvContext,
ULONG dwFlags);
Funkcja ta wstawia „element roboczy” do kolejki w¹tku w puli w¹tków i natychmiast wraca. Element roboczy to po prostu funkcja (identyfikowana przez parametr pfnCallback), która ma zostaæ wywo³ana z jednym parametrem – pvContext.
W koñcu któryœ z w¹tków puli przetworzy ten element roboczy, powoduj¹c wywo³anie podanej przez ciebie funkcji. Funkcja ta (wywo³ywana zwrotnie) musi
mieæ nastêpuj¹cy prototyp:
DWORD WINAPI WorkItemFunc(PVOID pvContext);
Chocia¿ prototyp tej funkcji musi definiowaæ wartoœæ zwrotn¹ jako DWORD,
w rzeczywistoœci jest ona i tak ignorowana.
Zwróæ uwagê, ¿e nigdy nie wywo³ujesz funkcji CreateThread bezpoœrednio. Pula
w¹tków dla twojego procesu jest tworzona automatycznie, a nastêpnie w¹tek
z tej puli wywo³uje twoj¹ funkcjê. Po obs³u¿eniu ¿¹dania klienta w¹tek ten nie
jest natychmiast usuwany. Wraca on z powrotem do puli w¹tków, gdzie czeka na
pobranie kolejnego elementu roboczego z kolejki. W ten sposób twoja aplikacja
staje siê bardziej efektywna, gdy¿ nie wymaga ju¿ tworzenia i usuwania w¹tku
dla ka¿dego pojedynczego ¿¹dania klienta. W dodatku, skoro w¹tki s¹ zwi¹zane
z portem zakoñczenia, liczba równolegle dzia³aj¹cych w¹tków mo¿e byæ co najwy¿ej dwa razy wiêksza od liczby CPU. To z kolei zmniejsza liczbê prze³¹czeñ
kontekstu.
A oto co siê dzieje w œrodku QueueUserWorkItem. Funkcja sprawdza liczbê
w¹tków w sk³adniku nie-I/O i w zale¿noœci od obci¹¿enia (liczby elementów roboczych w kolejce) dodaje do sk³adnika nowy w¹tek. Nastêpnie wykonuje
dzia³anie bêd¹ce odpowiednikiem wywo³ania PostQueuedCompletionStatus
i przekazuje informacjê o twoim elemencie roboczym do portu zakoñczenia I/O.
Na koniec w¹tek czekaj¹cy na zasygnalizowanie portu zakoñczenia pobiera wia-
346
Czêœæ II: Jak to dzia³a
domoœæ (wywo³uj¹c GetQueuedCompletionStatus) i wywo³uje twoj¹ funkcjê. Po
powrocie z tej ostatniej w¹tek ponownie wywo³uje GetQueuedCompletionStatus
i czeka na kolejny element roboczy.
Po umieszczeniu przez w¹tek ¿¹dania I/O w kolejce sterownika urz¹dzenia pula
w¹tków spodziewa siê czêstej obs³ugi asynchronicznych ¿¹dañ I/O. Podczas realizacji ¿¹dania I/O przez sterownik, w¹tek, który przekaza³ to ¿¹danie, nie jest
blokowany i mo¿e siê zaj¹æ nastêpnym. Asynchroniczne wejœcie/wyjœcie jest kluczem do tworzenia wysoko wydajnych, skalowalnych aplikacji, gdy¿ pozwala
jednemu w¹tkowi obs³ugiwaæ ¿¹dania przychodz¹ce od ró¿nych klientów – nie
wymaga szeregowego ich przetwarzania ani blokowania w czasie oczekiwania
na dokoñczenie bie¿¹cego ¿¹dania I/O.
System Windows nak³ada jednak pewne ograniczenia na asynchroniczne ¿¹dania I/O: jeœli w¹tek skieruje asynchroniczne ¿¹danie I/O do sterownika urz¹dzenia, a nastêpnie zakoñczy dzia³anie, jego ¿¹danie zgubi siê i ¿aden w¹tek nie zostanie powiadomiony o zrealizowaniu tego ¿¹dania. W dobrze zaprojektowanej
puli w¹tków liczba w¹tków roœnie i maleje w zale¿noœci od potrzeb klientów. Jeœli wiêc jakiœ w¹tek zg³asza asynchroniczne ¿¹danie I/O, a nastêpnie znika z powodu skurczenia siê puli, jego ¿¹danie I/O tak¿e znika. Taka sytuacja jest z regu³y niepo¿¹dana i wymaga jakiegoœ rozwi¹zania.
Jeœli chcesz umieœciæ w kolejce element roboczy, który wygeneruje asynchroniczne ¿¹danie I/O, nie mo¿esz przekazaæ tego elementu do sk³adnika nie-I/O puli
w¹tków. Musisz koniecznie u¿yæ do tego celu sk³adnika I/O. Sk³adnik I/O zawiera w¹tki, które nigdy nie gin¹, dopóki istniej¹ ich nieobs³u¿one ¿¹dania I/O.
W rezultacie powinieneœ u¿ywaæ ich tylko do wykonywania kodu generuj¹cego
asynchroniczne ¿¹dania I/O.
Aby wstawiæ element roboczy do kolejki sk³adnika I/O, równie¿ musisz wywo³aæ funkcjê QueueUserWorkItem, ale z parametrem dwFlags ustawionym na
WT_EXECUTEINIOTHREAD. Zazwyczaj przekazuje siê flagê WT_EXECUTEDEFAULT (zdefiniowan¹ jako 0), co powoduje skierowanie elementu roboczego
do w¹tków sk³adnika nie-I/O.
System Windows oferuje funkcje (takie jak RegNotifyChangeKeyValue), które
asynchronicznie wykonuj¹ zadania zwi¹zane z nie-I/O. Funkcje te równie¿ wymagaj¹, aby w¹tek wywo³uj¹cy nadal istnia³. Jeœli chcesz wywo³aæ jedn¹ z tych
funkcji za pomoc¹ sta³ego w¹tku z puli, mo¿esz u¿yæ flagi WT_EXECUTEINPERSISTENTTHREAD, która powoduje wywo³anie funkcji zwrotnej kolejkowanego
elementu roboczego przez w¹tek sk³adnika zegarowego. Poniewa¿ w¹tek sk³adnika zegarowego nigdy siê nie koñczy, gwarantuje on zrealizowanie asynchronicznej operacji. Powinieneœ upewniæ siê, czy twoja funkcja wywo³ywana zwrotnie nie grozi blokad¹ i wykonuje siê doœæ szybko, aby nie wp³yn¹æ ujemnie na
dzia³anie w¹tku sk³adnika zegarowego.
Dobrze zaprojektowana pula w¹tków musi siê równie¿ staraæ, aby przez ca³y
czas by³y dostêpne w¹tki do obs³ugi ¿¹dañ. Jeœli pula sk³ada siê z 4 w¹tków,
a w kolejce jest 100 elementów roboczych, tylko cztery elementy mog¹ byæ
Rozdzia³ 11: Pula w¹tków
347
obs³ugiwane jednoczeœnie. Nie stanowi to jeszcze problemu, jeœli obs³uga jednego elementu wymaga kilku milisekund, ale w przypadku d³u¿szego czasu mog¹
siê zacz¹æ tworzyæ zatory.
Oczywiœcie system nie jest doœæ inteligentny, aby przewidzieæ, co bêd¹ robiæ
funkcje poszczególnych elementów roboczych. Dlatego, jeœli wiesz, ¿e zajmie to
trochê czasu, powinieneœ wywo³ywaæ funkcjê QueueUserWorkItem z flag¹
WT_EXECUTELONGFUNCTION. Flaga ta pomaga puli w¹tków zdecydowaæ,
czy ma dodaæ nowy w¹tek – jeœli wszystkie dotychczasowe w¹tki s¹ zajête, wymusza utworzenie nowego. Tak wiêc wstawienie do kolejki jednoczeœnie 10 000
elementów roboczych z flag¹ WT_EXECUTELONGFUNCTION spowoduje dodanie do puli 10 000 w¹tków. Jeœli nie chcesz do tego dopuœciæ, musisz roz³o¿yæ
w czasie wywo³ania funkcji QueueUserWorkItem, aby choæ niektóre z elementów
roboczych mia³y szansê wykonaæ siê, zanim pojawi¹ siê nowe.
Pula w¹tków nie mo¿e na³o¿yæ górnego ograniczenia na liczbê swoich w¹tków,
gdy¿ grozi to zakleszczeniem. WyobraŸ sobie kolejkê 10 000 elementów roboczych czekaj¹cych na zdarzenie sygnalizowane przez element 10 001. Gdybyœ
ustawi³ limit na 10 000 w¹tków, 10 001 element nie móg³by siê wykonaæ i wszystkie 10 000 elementów by³oby zablokowanych na zawsze.
Korzystaj¹c z funkcji puli w¹tków powinieneœ wykrywaæ potencjalne zakleszczenia. Oczywiœcie musisz uwa¿aæ na blokowanie siê funkcji elementów roboczych z powodu sekcji krytycznych, semaforów, muteksów i tak dalej – takie sytuacje zwiêkszaj¹ niebezpieczeñstwo zakleszczeñ. Pamiêtaj zawsze, do którego
sk³adnika nale¿y w¹tek wykonuj¹cy twój kod (zegarowego, czekania, I/O czy
nie-I/O). Uwa¿aj te¿, czy funkcje elementu roboczego nie s¹ w module DLL, który mo¿e zostaæ dynamicznie usuniêty z pamiêci. W¹tek wywo³uj¹cy funkcjê
z usuniêtego modu³u DLL powoduje naruszenie praw dostêpu. Aby do tego nie
dopuœciæ, musisz prowadziæ licznik odwo³añ swoich elementów roboczych:
zwiêkszaæ jego wartoœæ po ka¿dym wywo³aniu QueueUserWorkItem i zmniejszaæ
po zakoñczeniu funkcji elementu roboczego. Tylko w przypadku zejœcia tego
licznika do 0 mo¿na bezpiecznie wy³adowaæ modu³ DLL.
Scenariusz 2: Wywo³ywanie funkcji w okreœlonych
odstêpach czasu
Czasami aplikacje musz¹ wykonywaæ pewne zadania w okreœlonych terminach.
W celu u³atwienia tego typu dzia³añ system Windows udostêpnia obiekt j¹dra
zwany czasomierzem. Wielu programistów tworzy oddzielne czasomierze dla
wszystkich terminowych zadañ wykonywanych przez aplikacjê, choæ jest to niepotrzebne i prowadzi do marnowania zasobów systemowych. Zamiast tego mo¿na utworzyæ pojedynczy czasomierz, ustawiæ go na najbli¿szy termin, a gdy ten
minie, ustawiæ na nastêpny i tak dalej. Jednak napisanie odpowiedniego kodu,
który by to realizowa³, nie jest takie proste. Na szczêœcie nowa funkcja puli
w¹tków potrafi zrobiæ to za ciebie.
348
Czêœæ II: Jak to dzia³a
Aby zleciæ wykonanie elementu roboczego w okreœlonym terminie, musisz najpierw utworzyæ kolejkê zegarow¹, wywo³uj¹c nastêpuj¹c¹ funkcjê:
HANDLE CreateTimerQueue();
Kolejka zegarowa odpowiada za organizacjê zbioru zegarów. WyobraŸ sobie na
przyk³ad jeden plik wykonywalny, który realizuje kilka us³ug. Ka¿da us³uga mo¿e
wymagaæ czasomierza pomagaj¹cego zarz¹dzaæ jej stanem, na przyk³ad kiedy
uznaæ, ¿e jakiœ klient przesta³ odpowiadaæ, kiedy zebraæ i uaktualniæ informacje
statystyczne i tak dalej. Tworzenie oddzielnego czasomierza i dedykowanego
w¹tku dla ka¿dej takiej us³ugi jest nieefektywne. Zamiast tego ka¿da us³uga mo¿e
mieæ w³asn¹ kolejkê zegarow¹ (niewielki zasób) i dzieliæ z innymi w¹tek sk³adnika
zegarowego oraz jego obiekt-czasomierz. Z chwil¹ zakoñczenia us³ugi wystarczy
usun¹æ jej kolejkê zegarow¹, a tym samym wszystkie utworzone przez ni¹ zegary.
Jeœli masz ju¿ kolejkê zegarow¹ i chcesz utworzyæ w niej zegar, musisz wywo³aæ
nastêpuj¹c¹ funkcjê:
BOOL CreateTimerQueueTimer(
PHANDLE phNewTimer,
HANDLE hTimerQueue,
WAITORTIMERCALLBACK pfnCallback,
PVOID pvContext,
DWORD dwDueTime,
DWORD dwPeriod,
ULONG dwFlags);
Drugi parametr przekazuje uchwyt kolejki zegarowej, w której chcesz utworzyæ
nowy zegar. Jeœli chcesz utworzyæ tylko kilka zegarów, mo¿esz ustawiæ hTimerQueue na NULL i pomin¹æ w ogóle wywo³anie CreateTimerQueue. Wartoœæ NULL
ka¿e funkcji u¿yæ standardowej kolejki zegarowej i upraszcza kodowanie. Parametry pfnCallback i pvContext wskazuj¹ funkcjê wywo³ywan¹ zwrotnie i argument przekazywany do niej w momencie wywo³ania. Parametr dwDueTime podaje liczbê milisekund do pierwszego wywo³ania tej funkcji. (Wartoœæ 0 oznacza
jak najszybsze wywo³anie funkcji, co upodabnia CreateTimerQueueTimer do funkcji QueueUserWorkItem). Parametr dwPeriod podaje liczbê milisekund miêdzy kolejnymi wywo³aniami funkcji. Wartoœæ 0 oznacza, ¿e jest to „jednorazowy” zegar,
czyli element roboczy ma byæ wstawiony do kolejki tylko raz. Parametr phNewTimer zwraca uchwyt nowego zegara utworzonego przez funkcjê.
Wywo³ywana zwrotnie funkcja robocza musi mieæ nastêpuj¹cy prototyp:
VOID WINAPI WaitOrTimerCallback(
PVOID pvContext,
BOOL fTimerOrWaitFired);
Po wywo³aniu tej funkcji parametr fTimerOrWaitFired podaje zawsze TRUE, co
oznacza w³¹czenie zegara.
Pomówmy teraz o parametrze dwFlags funkcji CreateTimerQueueTimer. Parametr
ten mówi funkcji, jak ma umieœciæ w kolejce element roboczy, gdy nadejdzie jego
czas. Jeœli element ma zostaæ przetworzony przez w¹tek sk³adnika nie-I/O, u¿yj
flagi WT_EXECUTEDEFAULT. Jeœli chcesz wygenerowaæ asynchroniczne ¿¹danie I/O, u¿yj flagi WT_EXECUTEINIOTHREAD. Jeœli chcesz przetworzyæ ele-
Rozdzia³ 11: Pula w¹tków
349
ment za pomoc¹ w¹tku, który nigdy nie znika, u¿yj flagi WT_EXECUTEINPERSISTENTTHREAD. Jeœli zaœ myœlisz, ¿e wykonanie elementu zajmie du¿o czasu,
u¿yj flagi WT_EXECUTELONGFUNCTION.
Mo¿esz równie¿ zastosowaæ flagê WT_EXECUTEINTIMERTHREAD, która jednak wymaga nieco wiêcej wyjaœnieñ. W zamieszczonej wczeœniej tabeli 11-1 poda³em, ¿e wœród sk³adników puli w¹tków jest równie¿ zegar. Tworzy on pojedynczy obiekt-czasomierz i zarz¹dza jego czasem aktywacji. Sk³adnik ten zawiera zawsze tylko jeden w¹tek. Wywo³anie funkcji CreateTimerQueueTimer budzi
w¹tek sk³adnika zegarowego, dodaje twój zegar do kolejki zegarów i resetuje
obiekt-czasomierz. Nastêpnie sk³adnik zegarowy przechodzi w stan uœpienia
czekaj¹c, a¿ czasomierz umieœci w jego kolejce wywo³anie APC. Gdy to nast¹pi
w¹tek budzi siê, aktualizuje kolejkê zegarów, resetuje czasomierz i decyduje, co
zrobiæ z elementem roboczym, którego czas wykonania w³aœnie nadszed³.
Aby ustaliæ, co robiæ z elementem roboczym, w¹tek sprawdza, czy jest ustawiona
flaga WT_EXECUTEDEFAULT, WT_EXECUTEINIOTHREAD, WT_EXECUTEINPERSISTENTTHREAD, WT_EXECUTELONGFUNCTION czy WT_EXECUTEINTIMERTHREAD. Teraz powinieneœ ju¿ rozumieæ, co oznacza flaga
WT_EXECUTEINTIMERTHREAD: powoduje wykonanie elementu roboczego
przez w¹tek sk³adnika zegarowego. Chocia¿ takie wykonanie elementu roboczego jest bardziej efektywne, jest równie¿ bardzo niebezpieczne! Jeœli funkcja elementu roboczego zablokuje siê na d³ugi czas, w¹tek sk³adnika zegarowego nie
bêdzie móg³ robiæ niczego innego. Czasomierz mo¿e nadal umieszczaæ nowe pozycje APC w kolejce w¹tku, a ten nie bêdzie w stanie ich obs³u¿yæ, dopóki nie
nast¹pi powrót z aktualnie wykonywanej funkcji. Jeœli planujesz przetwarzanie
kodu za pomoc¹ w¹tku zegarowego, kod ten powinien wykonywaæ siê szybko
i bez blokowania.
Flagi WT_EXECUTEINIOTHREAD, WT_EXECUTEINPERSISTENTTHREAD
oraz WT_EXECUTEINTIMERTHREAD wzajemnie siê wykluczaj¹. Jeœli nie przeka¿esz ¿adnej z tych flag (lub u¿yjesz flagi WT_EXECUTEDEFAULT), element
roboczy trafi do kolejki w¹tku sk³adnika nie-I/O. Ponadto jeœli jest podana flaga
WT_EXECUTEINTIMERTHREAD, powoduje to zignorowanie flagi WT_EXECUTELONGFUNCTION.
Jeœli jakiœ zegar nie jest ci ju¿ wiêcej potrzebny, powinieneœ usun¹æ go, wywo³uj¹c
nastêpuj¹c¹ funkcjê:
BOOL DeleteTimerQueueTimer(
HANDLE hTimerQueue,
HANDLE hTimer,
HANDLE hCompletionEvent);
Musisz wywo³ywaæ tê funkcjê nawet w przypadku jednorazowych zegarów, które spe³ni³y ju¿ swoje zadanie. Parametr hTimerQueue wskazuje kolejkê, do której
nale¿y usuwany zegar, a hTimer sam zegar (uchwyt ten pochodzi z wczeœniejszego wywo³ania CreateTimerQueueTimer).
Ostatni parametr, hCompletionEvent, mówi, co robiæ z nie wykonanymi elementami roboczymi wstawionymi do kolejki za przyczyn¹ usuwanego zegara. Jeœli
350
Czêœæ II: Jak to dzia³a
ustawisz parametr na INVALID_HANDLE_VALUE, funkcja DeleteTimerQueueTimer nie wróci do momentu przetworzenia wszystkich tych elementów roboczych. Pomyœl, co to oznacza: jeœli podczas przetwarzania elementu roboczego
wykonasz blokuj¹ce usuniêcie jego w³asnego zegara, dojdzie do zakleszczenia,
nieprawda¿? Bêdziesz czeka³ z usuniêciem zegara, a¿ element roboczy zakoñczy
dzia³anie, a jednoczeœnie zawiesisz jego przetwarzanie, czekaj¹c na usuniêcie tego zegara! Z tego wzglêdu w¹tek mo¿e wykonaæ blokuj¹ce usuwanie zegara tylko wtedy, gdy chodzi o zegar elementu roboczego przetwarzanego przez w¹tek.
Jeœli u¿ywasz w¹tku sk³adnika zegarowego, nie powinieneœ próbowaæ usuwaæ
w sposób blokuj¹cy ¿adnego zegara, gdy¿ grozi to zakleszczeniem. Próba usuniêcia zegara powoduje wstawienie do kolejki w¹tku sk³adnika zegarowego powiadomienia APC. Jeœli w¹tek ten czeka na usuniêcie zegara, nie mo¿e jednoczeœnie sam go usun¹æ, co oznacza zakleszczenie.
Zamiast ustawiaæ parametr hCompletionEvent na INVALID_HANDLE_VALUE,
mo¿esz go równie¿ ustawiæ na NULL. Oznacza to, ¿e chcesz usun¹æ zegar tak
szybko, jak to tylko mo¿liwe. W rezultacie powrót z DeleteTimerQueueTimer
nast¹pi natychmiast, ale nie bêdziesz wiedzia³, kiedy zakoñczy siê przetwarzanie
wszystkich elementów roboczych wstawionych do kolejki przez zegar. Mo¿esz
te¿ przekazaæ przez parametr hCompletionEvent uchwyt obiektu-zdarzenia. W takim przypadku powrót z DeleteTimerQueueTimer równie¿ nast¹pi natychmiast,
a w¹tek sk³adnika zegarowego zasygnalizuje to zdarzenie po zakoñczeniu przetwarzania wszystkich elementów roboczych wstawionych do kolejki przez zegar. Pilnuj, aby zdarzenie to nie by³o w stanie sygnalizowanym przed wywo³aniem DeleteTimerQueueTimer, gdy¿ inaczej funkcja potraktuje wszystkie elementy robocze jako zakoñczone, zanim to faktycznie nast¹pi.
Po utworzeniu zegara mo¿esz zmieniæ czas jego pierwszej aktywacji lub wielkoœæ
odstêpu miêdzy kolejnymi aktywacjami za pomoc¹ nastêpuj¹cej funkcji:
BOOL ChangeTimerQueueTimer(
HANDLE hTimerQueue,
HANDLE hTimer,
ULONG dwDueTime,
ULONG dwPeriod);
Parametry hTimerQueue i hTimer przekazuj¹ odpowiednio uchwyt kolejki zegarów oraz uchwyt zegara, którego ustawienia nale¿y zmodyfikowaæ. Nowe ustawienia podawane s¹ przez parametry dwDueTime i dwPeriod. Pamiêtaj, ¿e zmiana
ustawieñ jednorazowego zegara, który ju¿ zadzia³a³, niczego nie powoduje. Ponadto wywo³uj¹c tê funkcjê, nie musisz siê wcale obawiaæ o zakleszczenia.
Jeœli nie potrzebujesz ju¿ jakiegoœ zbioru zegarów, mo¿esz usun¹æ ich kolejkê,
wywo³uj¹c nastêpuj¹c¹ funkcjê:
BOOL DeleteTimerQueueEx(
HANDLE hTimerQueue,
HANDLE hCompletionEvent);
Rozdzia³ 11: Pula w¹tków
351
Funkcja ta pobiera uchwyt istniej¹cej kolejki zegarów i usuwa jej ca³¹ zawartoœæ,
dziêki czemu nie musisz wywo³ywaæ oddzielnie dla ka¿dego zegara funkcji DeleteTimerQueueTimer. Parametr hCompletionEvent dzia³a dok³adnie tak samo, jak
w przypadku DeleteTimerQueueTimer. Oznacza to niebezpieczeñstwo zakleszczeñ, dlatego b¹dŸ ostro¿ny.
Zanim przejdziemy do nastêpnego scenariusza, pozwól mi zwróciæ uwagê na kilka dodatkowych spraw. Po pierwsze sk³adnik zegarowy puli w¹tków tworzy
czasomierz, a ten wstawia do kolejki pozycje APC zamiast sygnalizowaæ obiekty.
To oznacza, ¿e system operacyjny przez ca³y czas kolejkuje pozycje APC i ¿adne
zdarzenie zegara nigdy nie przepadnie. Jeœli wiêc ustawisz zegar okresowy, który bêdzie siê w³¹cza³ co 10 sekund, masz gwarancjê, ¿e twoja funkcja wywo³ywana zwrotnie bêdzie uruchamiana faktycznie co 10 sekund. Pamiêtaj, ¿e bêdzie siê
to odbywaæ przy u¿yciu wielu w¹tków i mo¿e byæ konieczne synchronizowanie
fragmentów twojej funkcji elementu roboczego.
Jeœli taki scenariusz ci nie odpowiada i wolisz, aby ka¿dy nastêpny element roboczy by³ kolejkowany 10 sekund po wykonaniu poprzedniego, powinieneœ tworzyæ zegar jednorazowy na koñcu funkcji elementu roboczego. Mo¿esz te¿ utworzyæ pojedynczy zegar z wysokim limitem czasu i na koñcu funkcji elementu roboczego wywo³ywaæ funkcjê ChangeTimerQueueTimer.
Przyk³adowa aplikacja TimedMsgBox
Aplikacja TimedMsgBox („11 TimedMsgBox.exe”), pokazana w listingu 11-1,
przedstawia wykorzystanie funkcji zegarowych puli w¹tku do implementacji
pola komunikatu, które zamyka siê automatycznie w przypadku braku reakcji
u¿ytkownika przez okreœlony czas. Pliki z kodem Ÿród³owym i zasobami tej aplikacji mo¿na znaleŸæ w katalogu 11-TimedMsgBox na do³¹czonym do ksi¹¿ki
CD-ROM-ie.
Na pocz¹tku programu zostaje nadana wartoœæ 10 zmiennej globalnej g_nSecLeft.
Jest to liczba sekund, w ci¹gu których u¿ytkownik musi zareagowaæ na pole komunikatu. Nastêpnie wywo³ywana jest funkcja CreateTimerQueueTimer nakazuj¹ca puli w¹tków wywo³ywanie co sekundê funkcji MsgBoxTimeout. Po zakoñczeniu inicjalizacji dochodzi do wywo³ania funkcji MessageBox i na ekranie u¿ytkownika pojawia siê nastêpuj¹ce pole komunikatu:
Podczas czekania na reakcjê u¿ytkownika w¹tek puli w¹tków wywo³uje co sekundê funkcjê MsgBoxTimeout. Funkcja ta odnajduje uchwyt okna pola komunikatu, zmniejsza wartoœæ zmiennej globalnej g_nSecLeft o 1 i aktualizuje napis wi-
352
Czêœæ II: Jak to dzia³a
doczny w polu komunikatu. Po pierwszym wywo³aniu MsgBoxTimeout pole komunikatu powinno wygl¹daæ nastêpuj¹co:
Po dziesi¹tym wywo³aniu funkcji MsgBoxTimeout zmienna g_nSecLeft przyjmuje
wartoœæ 0 i dochodzi do wywo³ania funkcji EndDialog, która ma usun¹æ pole komunikatu. Nastêpuje powrót z MessageBox do g³ównego w¹tku i wywo³anie DeleteTimerQueueTimer nakazuj¹ce puli w¹tków zaprzestanie wywo³ywania funkcji
MsgBoxTimeout. Na ekranie pojawia siê inne pole komunikatu informuj¹ce u¿ytkownika, ¿e nie zareagowa³ na pierwsze pole komunikatu w wyznaczonym czasie.
Jeœli u¿ytkownik zareaguje przed up³ywem limitu czasu, na ekranie pojawi siê
nastêpuj¹ce pole komunikatu:
Listing 11-1. Przyk³adowa aplikacja TimedMsgBox
TimedMsgBox.cpp
/*************************************************************************
Modu³: TimedMsgBox.cpp
Uwagi: Copyright (c) 2000 Jeffrey Richter
*************************************************************************/
#include "..\CmnHdr.h"
#include <tchar.h>
/* Zobacz dodatek A. */
//////////////////////////////////////////////////////////////////////////////
Rozdzia³ 11: Pula w¹tków
353
// Tytu³ pola komunikatu
TCHAR g_szCaption[] = TEXT("Timed Message Box");
// Liczba sekund, przez które ma byæ wyœwietlane pole komunikatu
int g_nSecLeft = 0;
// ID okna STATIC dla pola komunikatu
#define ID_MSGBOX_STATIC_TEXT
0x0000ffff
//////////////////////////////////////////////////////////////////////////////
VOID WINAPI MsgBoxTimeout(PVOID pvContext, BOOLEAN fTimeout) {
// UWAGA: Z powodu warunków wyœcigu w¹tków mo¿liwe jest (choæ ma³o
// prawdopodobne), ¿e pole komunikatu nie bêdzie jeszcze utworzone,
// gdy tu dojdziemy.
HWND hwnd = FindWindow(NULL, g_szCaption);
if (hwnd != NULL) {
// Okno ju¿ jest; aktualizacja pozosta³ego czasu.
TCHAR sz[100];
wsprintf(sz, TEXT("You have %d seconds to respond"), g_nSecLeft--);
SetDlgItemText(hwnd, ID_MSGBOX_STATIC_TEXT, sz);
if (g_nSecLeft == 0) {
// Czas siê skoñczy³; wymuszenie zamkniêcia pola komunikatu.
EndDialog(hwnd, IDOK);
}
} else {
}
}
// Nie ma jeszcze okna; tym razem nic nie robimy.
// Spróbujemy ponownie za sekundê.
//////////////////////////////////////////////////////////////////////////////
int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) {
chWindows9xNotAllowed();
// Liczba sekund na reakcjê u¿ytkownika
g_nSecLeft = 10;
// Utworzenie wielokrotnego 1-sekundowego zegara, uaktywniaj¹cego
// siê pierwszy raz po sekundzie.
HANDLE hTimerQTimer;
CreateTimerQueueTimer(&hTimerQTimer, NULL, MsgBoxTimeout, NULL,
1000, 1000, 0);
// Wyœwietlenie pola komunikatu.
MessageBox(NULL, TEXT("You have 10 seconds to respond"),
g_szCaption, MB_OK);
354
Czêœæ II: Jak to dzia³a
// Anulowanie zegara i usuniêcie kolejki
DeleteTimerQueueTimer(NULL, hTimerQTimer, NULL);
// Informacja o tym, czy zareagowa³ u¿ytkownik, czy up³yn¹³ czas.
MessageBox(NULL,
(g_nSecLeft == 0) ? TEXT("Timeout") : TEXT("User responded"),
TEXT("Result"), MB_OK);
}
return(0);
//////////////////////////////// Koniec pliku /////////////////////////////////
TimedMsgBox.rc
//Microsoft Developer Studio generated resource script.
//
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (U.S.) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_TIMEDMSGBOX
ICON
DISCARDABLE
"TimedMsgBox.ico"
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE DISCARDABLE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE DISCARDABLE
Rozdzia³ 11: Pula w¹tków
355
BEGIN
"#include ""afxres.h""\r\n"
"\0"
END
3 TEXTINCLUDE DISCARDABLE
BEGIN
"\r\n"
"\0"
END
#endif
// APSTUDIO_INVOKED
#endif
// English (U.S.) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif
// not APSTUDIO_INVOKED
Scenariusz 3: Wywo³ywanie funkcji w momencie
zasygnalizowania pojedynczego obiektu j¹dra
Z badañ Microsoftu wynika, ¿e wiele aplikacji tworzy w¹tki tylko po to, aby czekaæ
na zasygnalizowanie obiektu j¹dra. Gdy to nast¹pi, w¹tek powiadamia o tym inny
w¹tek, a nastêpnie czeka w pêtli na ponowne zasygnalizowanie obiektu. Jest to
ogromne marnotrawstwo zasobów systemowych. Oczywiœcie koszty utworzenia
nowego w¹tku s¹ znacznie mniejsze ni¿ koszty utworzenia nowego procesu, ale jakieœ s¹. Ka¿dy w¹tek wymaga stosu oraz zu¿ywa mnóstwo instrukcji na swoje
utworzenie i usuniêcie. Dlatego powinieneœ zawsze staraæ siê to zminimalizowaæ.
Jeœli w momencie zasygnalizowania obiektu j¹dra chcesz skierowaæ do wykonania jakiœ element roboczy, mo¿esz u¿yæ jeszcze jednej nowej funkcji puli w¹tków:
BOOL RegisterWaitForSingleObject(
PHANDLE phNewWaitObject,
HANDLE hObject,
WAITORTIMERCALLBACK pfnCallback,
PVOID pvContext,
ULONG dwMilliseconds,
ULONG dwFlags);
Funkcja ta przekazuje swoje parametry sk³adnikowi czekania puli w¹tków.
W ten sposób informujesz sk³adnik, ¿e w momencie zasygnalizowania obiektu
j¹dra (identyfikowanego przez hObject) ma on wstawiæ do kolejki pewien element roboczy. Mo¿esz równie¿ wyznaczyæ limit czasu, który spowoduje wstawienie elementu roboczego do kolejki nawet wtedy, gdy obiekt j¹dra nie zostanie
356
Czêœæ II: Jak to dzia³a
zasygnalizowany w tym czasie. Limitem mo¿e byæ równie¿ wartoœæ 0 lub INFINITE. Zasadniczo funkcja ta dzia³a podobnie jak funkcja WaitForSingleObject
(omówiona w rozdziale 9). Po zarejestrowaniu „czekania” funkcja zwraca identyfikuj¹cy je uchwyt (przez parametr phNewWaitObject).
Sk³adnik czekania wykorzystuje w œrodku funkcjê WaitForMultipleObjects do czekania na zarejestrowane obiekty i podlega wszystkim ograniczeniom istniej¹cym
ju¿ dla tej funkcji. Jednym z nich jest niemo¿noœæ wielokrotnego czekania na ten
sam uchwyt. Jeœli wiêc chcesz zarejestrowaæ jakiœ obiekt wiele razy, musisz wywo³aæ funkcjê DuplicateHandle, a nastêpnie oddzielnie zarejestrowaæ oryginalny
uchwyt i jego duplikat. Oczywiœcie WaitForMultipleObjects czeka na zasygnalizowanie dowolnego obiektu, a nie wszystkich jednoczeœnie. Jeœli znasz tê funkcjê,
to wiesz, ¿e mo¿e ona czekaæ na co najwy¿ej 64 (MAXIMUM_WAIT_OBJECTS)
obiekty jednoczeœnie. Co siê wiêc stanie, jeœli u¿ywaj¹c funkcji RegisterWaitForSingleObject zarejestrujesz wiêcej ni¿ 64 obiekty? Sk³adnik czekania utworzy dodatkowy w¹tek, równie¿ wywo³uj¹cy WaitForMultipleObjects. W praktyce nowy
w¹tek musi zostaæ utworzony co 63 obiekty, poniewa¿ musi on tak¿e czekaæ na
obiekt-czasomierz kontroluj¹cy limity czasu.
Gdy nadchodzi czas wykonania elementu roboczego, zostaje on wstawiony standardowo do kolejki w¹tków sk³adnika nie-I/O. Gdy jeden z nich w koñcu siê
obudzi, wywo³a twoj¹ funkcjê, której prototyp musi mieæ nastêpuj¹c¹ postaæ:
VOID WINAPI WaitOrTimerCallbackFunc(
PVOID pvContext,
BOOLEAN fTimerOrWaitFired);
Jeœli up³yn¹³ limit czasu czekania, parametr fTimerOrWaitFired jest ustawiony na
TRUE, a jeœli zosta³ zasygnalizowany obiekt – na FALSE.
Jeœli chodzi o parametr dwFlags funkcji RegisterWaitForSingleObjects, mo¿esz ustawiæ go na WT_EXECUTEINWAITTHREAD, co powoduje wykonanie funkcji elementu roboczego przez jeden z w¹tków samego sk³adnika czekania. Jest to bardziej efektywne rozwi¹zanie, gdy¿ nie wymaga umieszczania elementu roboczego w kolejce sk³adnika nie-I/O. Z drugiej strony niesie ono ryzyko, gdy¿ w¹tek
sk³adnika czekania, który wykonuje twoj¹ funkcjê elementu roboczego, nie mo¿e
w tym czasie czekaæ na zasygnalizowanie innych obiektów. U¿ywaj wiêc tej flagi
tylko wtedy, gdy wiesz, ¿e funkcja elementu roboczego wykonuje siê szybko.
Jeœli twój element roboczy generuje asynchroniczne ¿¹danie I/O lub wykonuje
pewne dzia³anie przy u¿yciu nie koñcz¹cego siê nigdy w¹tku, mo¿esz przekazaæ
odpowiednio flagê WT_EXECUTEINIOTHREAD lub WT_EXECUTEINPERSISTENTTHREAD. Mo¿esz te¿ u¿yæ flagi WT_EXECUTELONGFUNCTION, która
informuje pulê w¹tków, ¿e wykonanie twojej funkcji mo¿e zaj¹æ du¿o czasu i byæ
mo¿e nale¿y poszerzyæ pulê o nowy w¹tek. Flaga ta nadaje siê tylko do elementów roboczych kierowanych do sk³adnika nie-I/O lub I/O. Pamiêtaj, aby nie wykonywaæ czasoch³onnych funkcji przy u¿yciu w¹tków sk³adnika czekania.
Rozdzia³ 11: Pula w¹tków
357
Ostatni¹ flag¹, o której powinieneœ wiedzieæ, jest WT_EXECUTEONLYONCE.
Powiedzmy, ¿e rejestrujesz czekanie na obiekt-proces. Po zasygnalizowaniu tego
obiektu pozostaje on nadal w tym stanie. W efekcie sk³adnik czekania bêdzie stale kolejkowa³ swoje elementy robocze. W przypadku obiektu-procesu takie
dzia³anie jest z regu³y niepo¿¹dane. Aby temu zapobiec, mo¿esz u¿yæ flagi
WT_EXECUTEONLYONCE, która ka¿e sk³adnikowi czekania przerwaæ czekanie na obiekt po jednokrotnym wykonaniu elementu roboczego.
Powiedzmy teraz, ¿e czekasz na automatycznie resetowany obiekt-zdarzenie. Po
zasygnalizowaniu tego obiektu przechodzi on w stan niesygnalizowany, a jego
element roboczy trafia do kolejki. W tym momencie obiekt jest nadal zarejestrowany i sk³adnik czekania ponownie czeka na jego zasygnalizowanie lub up³yw
wyznaczonego czasu (który jest liczony ponownie od 0). Jeœli nie chcesz ju¿, aby
sk³adnik czekania czeka³ na ten obiekt, musisz go wyrejestrowaæ. Obowi¹zuje to
nawet w przypadku flagi WT_EXECUTEONLYONCE, która ju¿ zadzia³a³a. Aby
wyrejestrowaæ czekanie, musisz wywo³aæ nastêpuj¹c¹ funkcjê:
BOOL UnregisterWaitEx(
HANDLE hWaitHandle,
HANDLE hCompletionEvent);
Pierwszy parametr, hWaitHandle, wskazuje zarejestrowane czekanie (wartoœæ powrotna z RegisterWaitForSingleObject), a drugi, hCompletionEvent, okreœla sposób
powiadomienia o wykonaniu wszystkich elementów roboczych ustawionych
w kolejce do tego „czekania”. Tak jak w przypadku DeleteTimerQueueTimer
mo¿esz przekazaæ NULL (jeœli nie chcesz powiadomienia), INVALID_HANDLE_VALUE (aby zablokowaæ wywo³anie do momentu wykonania wszystkich
elementów roboczych z kolejki) lub uchwyt obiektu-zdarzenia (sygnalizowanego w momencie wykonania wszystkich kolejkowanych elementów roboczych).
W przypadku wywo³ania nieblokuj¹cego, jeœli nie ma elementów roboczych
w kolejce, UnregisterWaitEx zwraca TRUE, a w przeciwnym wypadku FALSE
(GetLastError zwraca wówczas STATUS_PENDING).
I tym razem musisz uwa¿aæ, przekazuj¹c INVALID_HANDLE_VALUE do UnregisterWaitEx, aby nie doprowadziæ do zakleszczenia. Funkcja elementu roboczego nie powinna blokowaæ samej siebie, próbuj¹c wyrejestrowaæ czekanie, które
spowodowa³o wykonanie tego elementu roboczego. To tak jakby powiedzieæ: zawieœ moje wykonanie do momentu, a¿ skoñczê dzia³aæ – zakleszczenie nieuniknione. Funkcja UnregisterWaitEx jest jednak tak zaprojektowana, aby unikaæ zakleszczenia, gdy w¹tek sk³adnika czekania wykonuje jakiœ element roboczy i ten
element wyrejestrowuje czekanie, które spowodowa³o jego wykonanie. I jeszcze
jedna rzecz: nie zamykaj uchwytu obiektu j¹dra, dopóki nie wyrejestrujesz czekania. Zamkniêcie uchwytu powoduje jego uniewa¿nienie i w efekcie w¹tek
sk³adnika czekania wywo³uj¹cy w œrodku WaitForMultipleObjects przeka¿e nieprawid³owy uchwyt. W takiej sytuacji WaitForMultipleObjects natychmiast
zg³asza niepowodzenie i ca³y sk³adnik czekania zachowuje siê nieprawid³owo.
I na koniec pamiêtaj, aby nie wywo³ywaæ PulseEvent w celu zasygnalizowania zarejestrowanego obiektu-zdarzenia. Jeœli to zrobisz, w¹tek sk³adnika czekania
358
Czêœæ II: Jak to dzia³a
i tak bêdzie prawdopodobnie w tym czasie czymœ zajêty i chwilowe zasygnalizowanie nie zostanie zauwa¿one. Ten problem nie powinien byæ czymœ nowym dla
ciebie: dotyczy on PulseEvent w wiêkszoœci architektur w¹tkowych.
Scenariusz 4: Wywo³ywanie funkcji w momencie
zakoñczenia asynchronicznego ¿¹dania I/O
Ostatni scenariusz jest bardzo typowy: twoja aplikacja serwerowa generuje pewne asynchroniczne ¿¹dania I/O. Po zakoñczeniu generowania tych ¿¹dañ chcesz,
aby pula w¹tków by³a gotowa do ich przetworzenia. Jest to sytuacja, dla której
porty zakoñczenia I/O zosta³y pierwotnie zaprojektowane. Gdybyœ mia³ samodzielnie rozwi¹zaæ ten problem, utworzy³byœ port zakoñczenia I/O oraz pulê
w¹tków czekaj¹cych na zasygnalizowanie tego portu. Otworzy³byœ równie¿ grupê urz¹dzeñ I/O i skojarzy³ ich uchwyty z tym portem. W momencie zakoñczenia jakiegoœ ¿¹dania I/O odpowiedni sterownik urz¹dzenia wstawia³by „element roboczy” do kolejki portu zakoñczenia.
Jest to znakomity sposób efektywnego obs³ugiwania wielu elementów roboczych
przez kilka zaledwie w¹tków. Do tego okazuje siê, ¿e funkcje puli w¹tków maj¹
wbudowany ten mechanizm, dziêki czemu pozwalaj¹ zaoszczêdziæ mnóstwo
czasu i pracy. Aby skorzystaæ z tej architektury, jedyne co musisz zrobiæ, to otworzyæ urz¹dzenie i skojarzyæ je ze sk³adnikiem nie-I/O puli w¹tków. Pamiêtaj, ¿e
wszystkie w¹tki sk³adnika nie-I/O czekaj¹ na port zakoñczenia I/O. Aby skojarzyæ urz¹dzenie ze sk³adnikiem puli, musisz wywo³aæ nastêpuj¹c¹ funkcjê:
BOOL BindIoCompletionCallback(
HANDLE hDevice,
POVERLAPPED_COMPLETION_ROUTINE pfnCallback,
ULONG dwFlags);
Funkcja ta wywo³uje w œrodku funkcjê CreateIoCompletionPort i przekazuje do
niej swój parametr hDevice oraz uchwyt wewnêtrznego portu zakoñczenia. Ponadto wywo³anie BindIoCompletionCallback gwarantuje istnienie zawsze co najmniej jednego w¹tku w sk³adniku nie-I/O. Klucz zakoñczenia skojarzony z tym
urz¹dzeniem jest adresem zak³adkowej procedury zakoñczenia. W ten sposób,
gdy zakoñczy siê ¿¹danie I/O do tego urz¹dzenia, sk³adnik nie-I/O wie, któr¹
funkcjê wywo³aæ do przetworzenia tego ¿¹dania. Procedura zakoñczenia musi
mieæ nastêpuj¹cy prototyp:
VOID WINAPI OverlappedCompletionRoutine(
DWORD dwErrorCode,
DWORD dwNumberOfBytesTransferred,
POVERLAPPED pOverlapped);
Zwróæ uwagê, ¿e w wywo³aniu BindIoCompletionCallback nie podaje siê struktury
OVELAPPED. Struktura ta jest przekazywana do takich funkcji, jak ReadFile czy
WriteFile. System œledzi wewn¹trz tê strukturê razem z nie zakoñczonym ¿¹daniem I/O. Po zakoñczeniu ¿¹dania system umieszcza adres tej struktury w porcie zakoñczenia, aby zosta³ przekazany do twojej procedury OverlappedComple-
Rozdzia³ 11: Pula w¹tków
359
tionRoutine. W dodatku, poniewa¿ adres procedury zakoñczenia jest kluczem zakoñczenia, aby przekazaæ dodatkowe informacje kontekstowe do OverlappedCompletionRoutine, powinieneœ u¿yæ tradycyjnego triku i umieœciæ te informacje
na koñcu struktury OVERLAPPED.
Pamiêtaj te¿, ¿e zamkniêcie urz¹dzenia powoduje natychmiastowe zakoñczenie
b³êdem wszystkich nie zrealizowanych jeszcze ¿¹dañ I/O. Uwzglêdnij koniecznie ten przypadek w swojej funkcji wywo³ywanej zwrotnie. Jeœli po zamkniêciu
urz¹dzenia chcesz upewniæ siê, ¿e nie zosta³a wykonana ¿adna procedura wywo³ywana zwrotnie, musisz prowadziæ licznik odwo³añ w swojej aplikacji. Innymi s³owy musisz zwiêkszaæ licznik po ka¿dym wygenerowaniu ¿¹dania I/O
i zmniejszaæ go po ka¿dym zakoñczeniu takiego ¿¹dania.
Aktualnie nie ma ¿adnej specjalnej flagi, któr¹ móg³byœ przekazywaæ do BindIoCompletionCallback przez parametr dwFlags, tak wiêc musisz ustawiaæ go na 0.
Uwa¿am jednak, ¿e powinieneœ byæ w stanie przekazaæ WT_EXECUTEINIOTHREAD. Gdy ¿¹danie I/O zostaje zakoñczone, trafia do kolejki w¹tku
sk³adnika nie-I/O. W swojej funkcji OverlappedCompletionRoutine mo¿esz generowaæ jeszcze inne asynchroniczne ¿¹danie I/O. Ale pamiêtaj, ¿e zakoñczenie
w¹tku, który wygenerowa³ asynchroniczne ¿¹dania I/O, niszczy równie¿ te
¿¹dania. Ponadto w¹tki w sk³adniku nie-I/O s¹ tworzone i niszczone w zale¿noœci od obci¹¿enia. Jeœli obci¹¿enie jest ma³e, w¹tek sk³adnika mo¿e zakoñczyæ siê,
zanim zostan¹ obs³u¿one wszystkie ¿¹dania I/O. Gdyby funkcja BindIoCompletionCallback dopuszcza³a flagê WT_EXECUTEINIOTHREAD, w¹tek czekaj¹cy na
port zakoñczenia obudzi³by siê i skierowa³ wynik do w¹tku sk³adnika I/O. Poniewa¿ w¹tki te nigdy nie umieraj¹, jeœli maj¹ w kolejce ¿¹danie I/O, móg³byœ generowaæ ¿¹dania I/O bez obawy, ¿e zostan¹ usuniête.
Chocia¿ flaga WT_EXECUTEINIOTHREAD by³aby wygodna, mo¿na ³atwo
emulowaæ opisane dzia³anie. Wystarczy w funkcji OverlappedCompletionRoutine
wywo³aæ QueueUserWorkItem i przekazaæ do niej flagê WT_EXECUTEINIOTHREAD oraz niezbêdne dane (prawdopodobnie co najmniej strukturê
OVERLAPPED). W koñcu to i tak wszystko, co mog³aby dla ciebie zrobiæ funkcja
puli w¹tków.

Podobne dokumenty