2011-12-21 9 views
8

Piszę wielowątkowy serwer zgodny z POSIX w c/C++, który musi być w stanie akceptować, czytać i pisać do dużej liczby połączenia asynchronicznie. Serwer ma kilka wątków roboczych, które wykonują zadania i sporadycznie (i nieprzewidywalnie) dane kolejki do zapisania w gniazdach. Dane są również sporadycznie (i nieprzewidywalnie) zapisywane na gniazdach przez klientów, więc serwer musi również czytać asynchronicznie. Jednym z oczywistych sposobów jest przekazanie każdemu połączeniu wątku, który czyta i zapisuje z/do gniazda; jest to jednak brzydkie, ponieważ każde połączenie może utrzymywać się przez długi czas, a serwer może w związku z tym posiadać setki tysięcy wątków, aby śledzić połączenia.Oczekiwanie na warunek (pthread_cond_wait) i zmianę gniazda (wybierz) jednocześnie

Lepszym podejściem byłoby posiadanie pojedynczego wątku, który obsługuje całą komunikację za pomocą funkcji select()/pselect(). Oznacza to, że pojedynczy wątek czeka na każdym gnieździe, aby był czytelny, a następnie spawnuje zadanie do przetworzenia danych wejściowych, które będą obsługiwane przez pulę innych wątków za każdym razem, gdy dane wejściowe będą dostępne. Za każdym razem, gdy inne wątki robocze generują dane wyjściowe dla połączenia, zostaje ono umieszczone w kolejce, a wątek komunikacyjny czeka, aż to gniazdo będzie zapisywalne przed jego zapisaniem.

Problem polega na tym, że wątek komunikacyjny może czekać w funkcji select() lub pselect(), gdy dane wyjściowe są umieszczane w kolejce przez wątki robocze serwera. Możliwe, że jeśli żaden sygnał wejściowy nie nadejdzie przez kilka sekund lub minut, kolejka fragmentu wyjścia będzie czekać na zakończenie wątku komunikacyjnego select() ing. Nie powinno się to jednak zdarzyć - dane powinny zostać zapisane tak szybko, jak to możliwe.

W tej chwili widzę kilka rozwiązań, które są bezpieczne dla wątków. Jedną z nich jest zajęcie wątku komunikacyjnego - oczekiwanie na dane wejściowe i aktualizacja listy gniazd, które czekają na pisanie co dziesiątą część sekundy. Nie jest to optymalne, ponieważ wymaga dużego oczekiwania, ale zadziała. Inną opcją jest użycie pselect() i wysłanie sygnału USR1 (lub czegoś podobnego) za każdym razem, gdy nowe dane wyjściowe zostały umieszczone w kolejce, dzięki czemu wątek komunikacyjny może zaktualizować listę gniazd, na które czeka od razu status zapisu. Wolę to ostatnie, ale nadal nie lubię używania sygnału do czegoś, co powinno być warunkiem (pthread_cond_t). Jeszcze jedną opcją jest włączenie na liście deskryptorów plików, na których czeka (select) oczekiwany plik obojętny, w którym zapisujemy jeden bajt, aby zawsze trzeba było dodać gniazdo do zapisywalnego zestawu fd_set dla select(); to obudziłoby serwer komunikacyjny, ponieważ ten konkretny plik byłby czytelny, dzięki czemu wątek komunikacyjny mógł natychmiast zaktualizować jego zapisywalny zestaw fd_set.

Czuję intuicyjnie, że drugie podejście (z sygnałem) jest "najbardziej poprawnym" sposobem programowania serwera, ale jestem ciekawy, czy ktoś wie, który z powyższych jest najbardziej wydajny, ogólnie rzecz biorąc, czy którykolwiek z powyższych spowoduje warunki wyścigu, których nie znam, lub jeśli ktokolwiek wie o bardziej ogólnym rozwiązaniu tego problemu. To, czego naprawdę chcę, to funkcja pthread_cond_wait_and_select(), która pozwala wątkowi komunikacyjnemu oczekiwać zarówno na zmianę gniazd, jak i sygnał ze stanu.

Z góry dziękuję.

Odpowiedz

6

Jest to dość powszechny problem.

Jednym z często używanych rozwiązań jest posiadanie rur jako mechanizmu komunikacyjnego od wątków roboczych do wątku I/O. Po zakończeniu zadania wątek roboczy zapisuje wskaźnik do wyniku w rurze. Wątek I/O czeka na odczytanym końcu rury wraz z innymi gniazdami i deskryptorami plików, a gdy rura jest gotowa do odczytu, budzi się, pobiera wskaźnik do wyniku i kontynuuje wypychanie wyniku do połączenia klienta w -tryb blokowania.

Uwaga: ponieważ rura czyta i zapisuje mniej niż lub równa PIPE_BUF są atomowe, wskaźniki są zapisywane i odczytywane w jednym ujęciu. Można nawet mieć wiele wątków roboczych zapisujących wskaźniki w tej samej rurze ze względu na gwarancję atomowości.

3

Twoje drugie podejście jest czystszą drogą. Całkowicie normalne jest uwzględnianie niestandardowych zdarzeń na liście w przypadku takich zdarzeń, jak select lub . Oto, co robimy w moim obecnym projekcie, aby poradzić sobie z takimi zdarzeniami. Używamy również timerów (w systemie Linux timerfd_create) do okresowych zdarzeń.

W systemie Linux eventfd umożliwia tworzenie takich dowolnych zdarzeń użytkownika w tym celu - w ten sposób powiedziałbym, że jest to dość akceptowana praktyka. W przypadku POSIX-a tylko funkcje, no, hmm, być może jedno z poleceń potoku lub socketpair.

Przepychanie za pomocą odpytywania nie jest dobrym rozwiązaniem.Najpierw skanujesz pamięć, która będzie używana przez inne wątki, powodując rywalizację o pamięć procesora. Po drugie, zawsze będziesz musiał powrócić do swojego połączenia select, które utworzy ogromną liczbę wywołań systemowych i przełączników kontekstowych, które zaszkodzą ogólnej wydajności systemu.

3

Niestety, najlepszy sposób na to jest inny dla każdej platformy. Kanonicznym, przenośnym sposobem na to jest posiadanie twojego bloku wątków I/O w poll. Jeśli chcesz, aby wątek I/O opuścił poll, wysyłasz pojedynczy bajt na pipe, który wątek pobiera. To spowoduje natychmiastowe wyjście wątku z poll.

W systemie Linux najlepszym rozwiązaniem jest wersja epoll. W systemach operacyjnych pochodnych BSD (w tym OSX, myślę), kqueue. Na Solarisie było to /dev/poll i jest coś jeszcze, którego imię zapominam.

Możesz po prostu rozważyć użycie biblioteki takiej jak libevent lub Boost.Asio. Dają ci najlepszy model we/wy na każdej obsługiwanej platformie.

Powiązane problemy