Porty zakończeniowe we/wy

Porty uzupełniania we/wy zapewniają wydajny model wątkowania do przetwarzania wielu asynchronicznych żądań we/wy w systemie wieloprocesorowym. Gdy proces tworzy port ukończenia operacji wejścia/wyjścia, system tworzy skojarzony z nim obiekt kolejki przeznaczony dla wątków, których jedynym celem jest obsługa tych żądań. Procesy obsługujące wiele współbieżnych żądań we/wy asynchronicznych mogą wykonywać to szybciej i wydajniej przy użyciu portów uzupełniania we/wy w połączeniu ze wstępnie przydzieloną pulą wątków niż przez utworzenie wątków w momencie otrzymania żądania we/wy.

Kiedy używać portów zakończenia operacji wejścia/wyjścia

Scenario Zalecane podejście
Serwer o wysokiej wydajności obsługuje setki/tysiące współbieżnych połączeń Porty ukończenia operacji we/wy — zaprojektowane specjalnie do tego celu. Jądro zarządza planowaniem wątków w celu dopasowania do współbieżności procesora CPU.
Umiarkowana współbieżność (dziesiątki operacji asynchronicznych) Operacje wejścia/wyjścia puli wątków (CreateThreadpoolIo) — prostsze API, wewnętrznie zarządza mechanizmem IOCP. Preferuj nowy kod, który nie wymaga ręcznego sterowania wątkami.
Proste operacje na plikach asynchronicznych w nowoczesnym języku C++ Korutyny C++20 z niestandardowym dyspozytorem IOCP lub .NET FileStream z async/await.
Jednowątkowe lub o niskim poziomie operacji we/wy Synchroniczne operacje we/wy lub proste asynchroniczne operacje we/wy z sygnalizowaniem zdarzeń. IOCP wprowadza niepotrzebną złożoność w scenariuszach z pojedynczym strumieniem.

Uwaga

Interfejs API puli wątków a bezpośrednie użycie IOCP: Interfejs API puli wątków systemu Windows (CreateThreadpoolIo, StartThreadpoolIo) wewnętrznie wykorzystuje IOCP, ale automatycznie obsługuje zarządzanie cyklem życia wątków. W przypadku nowych aplikacji serwerowych należy najpierw rozważyć API puli wątków — zapewnia ono tę samą skalowalność przy mniejszej ilości kodu szablonowego. Używaj surowego IOCP, gdy musisz jawnie kontrolować wartość współbieżności portu zakończeń lub zarządzać wątkami w niestandardowy sposób.

Jak działają porty uzupełniania we/wy

Funkcja CreateIoCompletionPort tworzy port uzupełniania we/wy i kojarzy jeden lub więcej dojść do pliku z tym portem. Gdy asynchroniczna operacja we/wy na jednym z tych uchwytów plików zostanie ukończona, pakiet zakończenia operacji we/wy jest umieszczany w kolejce skojarzonego portu zakończenia operacji we/wy w kolejności pierwsze weszło, pierwsze wyszło (FIFO). Jednym z zaawansowanych zastosowań tego mechanizmu jest połączenie punktu synchronizacji dla wielu dojść plików w jeden obiekt, chociaż istnieją również inne przydatne aplikacje. Należy pamiętać, że chociaż pakiety są umieszczane w kolejce w kolejności FIFO, mogą być z niej usuwane w innej kolejności.

Uwaga

Termin dojście do pliku, jak użyto tutaj, odnosi się do abstrakcji systemu reprezentującej nakładający się punkt końcowy we/wy, a nie tylko plik na dysku. Na przykład może to być sieciowy punkt końcowy, gniazdo TCP, potok nazwany lub gniazdo pocztowe. Można użyć dowolnego obiektu systemowego obsługującego nakładające się we/wy. Aby uzyskać listę powiązanych funkcji wejścia/wyjścia, zobacz końcową część tego tematu.

Gdy uchwyt pliku jest skojarzony z portem zakończeń, przekazany blok stanu nie zostanie zaktualizowany, dopóki pakiet nie zostanie usunięty z portu zakończeń. Jedynym wyjątkiem jest sytuacja, gdy oryginalna operacja kończy się synchronicznie błędem. Wątek (utworzony przez wątek główny lub sam wątek główny) używa funkcji GetQueuedCompletionStatus do oczekiwania na umieszczenie pakietu zakończenia w kolejce portu zakończeń we/wy, zamiast bezpośrednio oczekiwać na zakończenie asynchronicznej operacji we/wy. Wątki, które wstrzymują swoje wykonywanie na porcie ukończenia operacji we/wy, są zwalniane w kolejności ostatni na wejściu, pierwszy na wyjściu (LIFO), a dla tego wątku następny pakiet ukończenia jest pobierany z kolejki FIFO portu ukończenia operacji we/wy. Oznacza to, że gdy pakiet zakończenia zostaje przekazany do wątku, system uaktywnia ostatni (najnowszy) wątek skojarzony z tym portem, przekazując mu informacje o zakończeniu najstarszej operacji we/wy.

Mimo że dowolna liczba wątków może wywołać metodę GetQueuedCompletionStatus dla określonego portu uzupełniania we/wy, gdy określony wątek wywołuje metodę GetQueuedCompletionStatus po raz pierwszy, staje się skojarzony z określonym portem uzupełniania we/wy do momentu wystąpienia jednego z trzech elementów: wątek kończy działanie, określa inny port uzupełniania we/wy lub zamyka port uzupełniania we/wy. Innymi słowy, pojedynczy wątek może być skojarzony co najwyżej z jednym portem zakończenia operacji we/wy.

Gdy pakiet zakończenia zostaje umieszczony w kolejce portu zakończeń operacji we/wy, system najpierw sprawdza, ile wątków skojarzonych z tym portem jest uruchomionych. Jeśli liczba uruchomionych wątków jest mniejsza niż wartość współbieżności (omówiona w następnej sekcji), jeden z wątków oczekujących (najnowszych) może przetworzyć pakiet ukończenia. Po zakończeniu przetwarzania uruchomionego wątku zwykle wywołuje ponownie metodę GetQueuedCompletionStatus , w którym zwracany jest następny pakiet ukończenia lub czeka, jeśli kolejka jest pusta.

Wątki mogą używać funkcji PostQueuedCompletionStatus do umieszczania pakietów uzupełniania w kolejce portu uzupełniania we/wy. Dzięki temu port zakończeń może być używany do odbierania komunikatów od innych wątków procesu, oprócz odbierania z systemu we/wy pakietów zakończenia operacji we/wy. Funkcja PostQueuedCompletionStatus umożliwia aplikacji umieszczanie własnych pakietów zakończenia specjalnego typu w kolejce portu zakończeń operacji we/wy bez rozpoczynania asynchronicznej operacji we/wy. Jest to przydatne na przykład do powiadamiania wątków roboczych o zdarzeniach zewnętrznych.

Uchwyt portu zakończenia operacji we/wy i każdy uchwyt pliku skojarzony z tym konkretnym portem zakończenia operacji we/wy są określane jako odwołania do portu zakończenia operacji we/wy. Port zakończenia operacji wejścia/wyjścia zostaje zwolniony, gdy nie ma już do niego żadnych odwołań. W związku z tym wszystkie te uchwyty muszą zostać prawidłowo zamknięte, aby zwolnić port zakończeń operacji wejścia/wyjścia i powiązane z nim zasoby systemowe. Po spełnieniu tych warunków aplikacja powinna zamknąć uchwyt portu zakończenia we/wy przez wywołanie funkcji CloseHandle.

Uwaga

Port zakończenia operacji wejścia/wyjścia jest skojarzony z procesem, który go utworzył, i nie może być współużytkowany między procesami. Jednak pojedynczy uchwyt może być współdzielony przez wątki w tym samym procesie.

Wątki i współbieżność

Najważniejszą właściwością portu ukończenia operacji we/wy, na którą należy zwrócić szczególną uwagę, jest wartość współbieżności. Wartość współbieżności portu ukończenia jest określana podczas jego tworzenia za pomocą polecenia CreateIoCompletionPort za pośrednictwem parametru NumberOfConcurrentThreads . Ta wartość ogranicza liczbę wątków możliwych do uruchomienia skojarzonych z portem uzupełniania. Gdy łączna liczba wątków możliwych do uruchomienia skojarzonych z portem ukończenia osiągnie wartość współbieżności, system blokuje wykonywanie wszystkich kolejnych wątków skojarzonych z tym portem ukończenia, dopóki liczba wątków możliwych do uruchomienia spadnie poniżej wartości współbieżności.

Najbardziej wydajny scenariusz występuje, gdy w kolejce oczekują pakiety ukończenia, ale nie można oczekiwać, ponieważ port osiągnął limit współbieżności. Zastanów się, co dzieje się z wartością współbieżności wynoszącą jeden oraz z wieloma wątkami oczekującymi w wywołaniu funkcji GetQueuedCompletionStatus. W takim przypadku, jeśli w kolejce zawsze oczekują pakiety zakończenia, to gdy wykonywany wątek wywoła GetQueuedCompletionStatus, wykonanie nie zostanie zablokowane, ponieważ, jak wspomniano wcześniej, kolejka wątków działa zgodnie z zasadą LIFO. Zamiast tego ten wątek natychmiast odbierze następny pakiet ukończenia w kolejce. Nie dojdzie do żadnych przełączeń kontekstu między wątkami, ponieważ wykonywany wątek stale pobiera pakiety zakończenia, a pozostałe wątki nie mogą zostać uruchomione.

Uwaga

W poprzednim przykładzie dodatkowe wątki wydają się bezużyteczne i nigdy nie działają, ale takie założenie oznacza, że działający wątek nigdy nie przejdzie w stan oczekiwania wskutek działania innego mechanizmu, nie zakończy działania ani w żaden inny sposób nie zamknie skojarzonego z nim portu zakończenia operacji we/wy. Podczas projektowania aplikacji należy wziąć pod uwagę wszystkie takie konsekwencje wykonywania wątków.

Najlepszą ogólną wartością maksymalną dla ustawienia współbieżności jest liczba procesorów w komputerze. Jeśli transakcja wymaga długiego obliczenia, większa wartość współbieżności umożliwi uruchomienie większej liczby wątków. Przetworzenie każdego pakietu uzupełniania może potrwać dłużej, ale w tym samym czasie będzie przetwarzanych więcej pakietów uzupełniania. Możesz eksperymentować z wartością współbieżności w połączeniu z narzędziami profilowania, aby uzyskać najlepszy efekt dla aplikacji.

System umożliwia również wątkowi oczekującemu w metodzie GetQueuedCompletionStatus przetwarzanie pakietu zakończenia, jeśli inny działający wątek skojarzony z tym samym portem zakończenia we/wy wchodzi w stan oczekiwania z innych powodów, na przykład z powodu funkcji SuspendThread. Gdy wątek w stanie oczekiwania zacznie działać ponownie, może wystąpić krótki okres, gdy liczba aktywnych wątków przekracza wartość współbieżności. Jednak system szybko zmniejsza tę liczbę, nie zezwalając na żadne nowe aktywne wątki, dopóki liczba aktywnych wątków nie spadnie poniżej wartości współbieżności. To jeden z powodów, aby aplikacja tworzyła w swojej puli wątków więcej wątków niż wynosi poziom współbieżności. Zarządzanie pulą wątków wykracza poza zakres tego zagadnienia, ale dobrą zasadą jest, aby w puli wątków było co najmniej dwa razy więcej wątków niż procesorów w systemie. Aby uzyskać dodatkowe informacje na temat buforowania wątków, zobacz Thread Pools.

Obsługiwane funkcje wejścia/wyjścia

Poniższe funkcje mogą służyć do uruchamiania operacji we/wy, które zakończą się przy użyciu portów uzupełniania we/wy. Aby włączyć mechanizm portu uzupełniania I/O, należy przekazać funkcji wystąpienie struktury OVERLAPPED oraz uchwyt pliku, który wcześniej został skojarzony z portem uzupełniania I/O (przez wywołanie CreateIoCompletionPort):

Informacje o procesach i wątkach

BindIoCompletionCallback

CreateIoCompletionPort