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.