Jest to luźno związane z tym pytaniem: czy std :: thread jest w puli w C ++ 11? . Chociaż pytanie jest inne, intencja jest taka sama:
Pytanie 1: Czy nadal ma sens używanie własnych (lub biblioteki innej firmy) pul wątków, aby uniknąć kosztownego tworzenia wątków?
Wniosek z drugiego pytania był taki, że nie można liczyć na std::thread
to, że zostaniemy połączeni (może lub nie). std::async(launch::async)
Wydaje się jednak, że ma znacznie większą szansę na połączenie.
Nie wydaje mi się, że jest to wymuszone przez standard, ale IMHO spodziewałbym się, że wszystkie dobre implementacje C ++ 11 będą używać puli wątków, jeśli tworzenie wątków jest powolne. Tylko na platformach, na których tworzenie nowego wątku jest niedrogie, spodziewałbym się, że zawsze tworzą nowy wątek.
Pytanie 2: Tak właśnie myślę, ale nie mam faktów, aby to udowodnić. Mogę się bardzo mylić. Czy to zgadywanie?
Na koniec przedstawiłem tutaj przykładowy kod, który najpierw pokazuje, jak myślę, że tworzenie wątków można wyrazić za pomocą async(launch::async)
:
Przykład 1:
thread t([]{ f(); });
// ...
t.join();
staje się
auto future = async(launch::async, []{ f(); });
// ...
future.wait();
Przykład 2: odpal i zapomnij wątek
thread([]{ f(); }).detach();
staje się
// a bit clumsy...
auto dummy = async(launch::async, []{ f(); });
// ... but I hope soon it can be simplified to
async(launch::async, []{ f(); });
Pytanie 3: Czy wolisz async
wersje od thread
wersji?
Reszta nie jest już częścią pytania, ale tylko dla wyjaśnienia:
Dlaczego wartość zwracana musi być przypisana do zmiennej fikcyjnej?
Niestety, obecny standard C ++ 11 wymusza przechwycenie zwracanej wartości std::async
, ponieważ w przeciwnym razie wykonywany jest destruktor, który blokuje się do momentu zakończenia akcji. Niektórzy uważają to za błąd w normie (np. Przez Herba Suttera).
Ten przykład z cppreference.com ładnie to ilustruje:
{
std::async(std::launch::async, []{ f(); });
std::async(std::launch::async, []{ g(); }); // does not run until f() completes
}
Kolejne wyjaśnienie:
Wiem, że pule wątków mogą mieć inne uzasadnione zastosowania, ale w tym pytaniu interesuje mnie tylko aspekt unikania kosztownych kosztów tworzenia wątków .
Myślę, że nadal istnieją sytuacje, w których pule wątków są bardzo przydatne, zwłaszcza jeśli potrzebujesz większej kontroli nad zasobami. Na przykład serwer może zdecydować o obsłudze tylko określonej liczby żądań jednocześnie, aby zagwarantować szybkie czasy odpowiedzi i zwiększyć przewidywalność wykorzystania pamięci. Pule wątków powinny być w porządku.
Zmienne lokalne wątku mogą być również argumentem dla twoich własnych pul wątków, ale nie jestem pewien, czy ma to zastosowanie w praktyce:
- Tworzenie nowego wątku ze
std::thread
startami bez zainicjowanych zmiennych lokalnych wątku. Może nie tego chcesz. - W wątkach utworzonych przez program
async
jest dla mnie niejasny, ponieważ wątek mógł zostać ponownie użyty. Z mojego zrozumienia nie ma gwarancji, że zmienne lokalne wątku zostaną zresetowane, ale mogę się mylić. - Z drugiej strony korzystanie z własnych pul wątków (o stałym rozmiarze) zapewnia pełną kontrolę, jeśli naprawdę tego potrzebujesz.
źródło
std::async(launch::async)
Wydaje się jednak, że ma znacznie większe szanse na połączenie." Nie, uważam,std::async(launch::async | launch::deferred)
że to może być połączone. Samolaunch::async
zadanie ma zostać uruchomione w nowym wątku, niezależnie od tego, jakie inne zadania są uruchomione. Dzięki politycelaunch::async | launch::deferred
implementacja wybiera, którą politykę, ale co ważniejsze, może opóźnić wybór tej polityki. Oznacza to, że może poczekać, aż wątek w puli wątków stanie się dostępny, a następnie wybrać zasadę asynchronizacji.std::async()
. Nadal jestem ciekawy, jak obsługują nietrywialne destruktory thread_local w puli wątków.launch::async
to traktuje ją tak, jakby była tylkolaunch::deferred
i nigdy nie wykonuje jej asynchronicznie - w efekcie ta wersja libstdc ++ „wybiera” zawsze używać odroczonego, chyba że wymuszono inaczej.std::async
mógł być piękną rzeczą dla wydajności - mógł to być standardowy system wykonywania krótkich zadań, naturalnie wspierany przez pulę wątków. W tej chwili jest to tylkostd::thread
z przyczepionymi bzdurami, aby funkcja wątku mogła zwrócić wartość. Aha, i dodali zbędną „odroczoną” funkcjonalność, którastd::function
całkowicie pokrywa się z zadaniem .Odpowiedzi:
Pytanie 1 :
Zmieniłem to z oryginału, ponieważ oryginał był zły. Miałem wrażenie, że tworzenie wątków w Linuksie jest bardzo tanie i po przetestowaniu stwierdziłem, że narzut wywołania funkcji w nowym wątku w porównaniu z normalnym jest ogromny. Narzut związany z utworzeniem wątku do obsługi wywołania funkcji jest około 10000 lub więcej razy wolniejszy niż zwykłe wywołanie funkcji. Tak więc, jeśli wydajesz wiele małych wywołań funkcji, dobrym pomysłem może być pula wątków.
Jest całkiem oczywiste, że standardowa biblioteka C ++ dostarczana z g ++ nie ma pul wątków. Ale zdecydowanie widzę dla nich uzasadnienie. Nawet biorąc pod uwagę narzut związany z koniecznością przepchnięcia połączenia przez jakąś kolejkę między wątkami, prawdopodobnie byłoby to tańsze niż rozpoczęcie nowego wątku. A standard na to pozwala.
IMHO, ludzie z jądrem Linuksa powinni popracować nad tym, aby tworzenie wątków było tańsze niż obecnie. Ale standardowa biblioteka C ++ powinna również rozważyć użycie puli do implementacji
launch::async | launch::deferred
.A OP jest poprawny, użycie
::std::thread
do uruchomienia wątku oczywiście wymusza utworzenie nowego wątku zamiast używania jednego z puli. Więc::std::async(::std::launch::async, ...)
jest preferowany.Pytanie 2 :
Tak, w zasadzie to „niejawnie” uruchamia wątek. Ale tak naprawdę nadal jest całkiem oczywiste, co się dzieje. Więc nie sądzę, aby to słowo w sposób dorozumiany było szczególnie dobrym słowem.
Nie jestem też przekonany, że zmuszanie Cię do czekania na powrót przed zniszczeniem jest koniecznie błędem. Nie wiem, czy powinieneś używać
async
wywołania do tworzenia wątków „demonów”, które nie powinny powrócić. A jeśli oczekuje się, że powrócą, ignorowanie wyjątków nie jest w porządku.Pytanie 3 :
Osobiście lubię, gdy uruchamianie wątków jest jednoznaczne. Przywiązuję dużą wagę do wysp, na których możesz zagwarantować dostęp szeregowy. W przeciwnym razie skończysz ze zmiennym stanem, że zawsze musisz gdzieś zawijać muteks i pamiętać o jego użyciu.
Model kolejki roboczej podobał mi się o wiele lepiej niż model „przyszły”, ponieważ istnieją „wyspy szeregowe”, dzięki czemu można efektywniej obsługiwać stan zmienny.
Ale tak naprawdę to zależy od tego, co dokładnie robisz.
Test wydajności
Więc przetestowałem wydajność różnych metod wywoływania rzeczy i wymyśliłem te liczby na 8-rdzeniowym (AMD Ryzen 7 2700X) systemie z Fedorą 29 skompilowanym z clang w wersji 7.0.1 i libc ++ (nie libstdc ++):
I natywny, na moim 15-calowym MacBooku Pro (Intel (R) Core (TM) i7-7820HQ, 2,90 GHz) z systemem
Apple LLVM version 10.0.0 (clang-1000.10.44.4)
OSX 10.13.6, otrzymuję to:W przypadku wątku roboczego uruchomiłem wątek, a następnie użyłem kolejki bez blokad do wysyłania żądań do innego wątku, a następnie czekałem na odesłanie odpowiedzi „Gotowe”.
„Nic nie rób” to po prostu przetestowanie napowietrznej uprzęży testowej.
Oczywiste jest, że koszt uruchomienia wątku jest ogromny. Nawet wątek roboczy z kolejką między wątkami spowalnia działanie o współczynnik około 20 w Fedorze 25 na maszynie wirtualnej i około 8 w natywnym systemie OS X.
Stworzyłem projekt Bitbucket zawierający kod, którego użyłem do testu wydajności. Można go znaleźć tutaj: https://bitbucket.org/omnifarious/launch_thread_performance
źródło