FixedThreadPool vs CachedThreadPool: mniejsze zło

97

Mam program, który generuje wątki (~ 5-150), które wykonują kilka zadań. Początkowo użyłem a, FixedThreadPoolponieważ to podobne pytanie sugerowało, że lepiej nadają się do dłuższych zadań, a przy mojej bardzo ograniczonej wiedzy na temat wielowątkowości, uznałem średni czas życia wątków (kilka minut) za „ długi czas ”.

Jednak ostatnio dodałem możliwość tworzenia dodatkowych wątków i dzięki temu przekraczam ustawiony przeze mnie limit wątków. Czy w takim przypadku lepiej byłoby zgadnąć i zwiększyć liczbę wątków, na które mogę zezwolić, czy przełączyć się na CachedThreadPooltak, aby nie było zmarnowanych wątków?

Wypróbowując oba na wstępie, nie wydaje się, żeby było różnica, więc jestem skłonny iść z nimi CachedThreadPooltylko po to, aby uniknąć marnotrawstwa. Jednak czy żywotność nici oznacza, że ​​powinienem zamiast tego FixedThreadPoolwybrać i zająć się nieużywanymi wątkami? To pytanie sprawia, że ​​wydaje się, że te dodatkowe wątki nie są zmarnowane, ale byłbym wdzięczny za wyjaśnienie.

Daniel
źródło

Odpowiedzi:

112

CachedThreadPool jest dokładnie tym, czego powinieneś używać w swojej sytuacji, ponieważ nie ma żadnych negatywnych konsekwencji używania go w przypadku długo działających wątków. Komentarz w dokumencie java dotyczący przydatności CachedThreadPools do krótkich zadań jedynie sugeruje, że są one szczególnie odpowiednie w takich przypadkach, a nie to, że nie mogą lub nie powinny być używane do zadań obejmujących długotrwałe zadania.

Aby rozwinąć dalej, Executors.newCachedThreadPool i Executors.newFixedThreadPool są obsługiwane przez tę samą implementację puli wątków (przynajmniej w otwartym JDK) tylko z różnymi parametrami. Różnice to po prostu ich minimum, maksimum, czas zabicia wątku i typ kolejki.

public static ExecutorService newFixedThreadPool(int nThreads) {
     return new ThreadPoolExecutor(nThreads, nThreads,
                                   0L, TimeUnit.MILLISECONDS,
                                   new LinkedBlockingQueue<Runnable>());
 }

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                 60L, TimeUnit.SECONDS,
                                 new SynchronousQueue<Runnable>());
}

FixedThreadPool ma swoje zalety, gdy w rzeczywistości chcesz pracować ze stałą liczbą wątków, ponieważ wtedy możesz przesłać dowolną liczbę zadań do usługi wykonawczej, wiedząc, że liczba wątków zostanie utrzymana na określonym przez Ciebie poziomie. Jeśli wyraźnie chcesz zwiększyć liczbę wątków, nie jest to właściwy wybór.

Oznacza to jednak, że jedynym problemem, który możesz mieć z CachedThreadPool, jest ograniczenie liczby wątków, które działają jednocześnie. CachedThreadPool nie ograniczy ich za Ciebie, więc może być konieczne napisanie własnego kodu, aby upewnić się, że nie uruchomisz zbyt wielu wątków. To naprawdę zależy od projektu Twojej aplikacji i sposobu przesyłania zadań do usługi wykonawcy.

Trevor Freeman
źródło
2
„CachedThreadPool jest dokładnie tym, czego powinieneś używać w swojej sytuacji, ponieważ nie ma żadnych negatywnych konsekwencji używania go w przypadku długo działających wątków”. Chyba się nie zgadzam. CachedThreadPool dynamicznie tworzy wątki bez górnego limitu. Długotrwałe zadania na dużej liczbie wątków mogą potencjalnie obciążać wszystkie zasoby. Ponadto posiadanie większej liczby wątków niż idealna może powodować marnowanie zbyt wielu zasobów na przełączanie kontekstu tych wątków. Chociaż na końcu odpowiedzi wyjaśniłeś, że wymagane jest niestandardowe ograniczanie przepustowości, początek odpowiedzi jest nieco mylący.
Nishit
1
Dlaczego nie wystarczy utworzyć ograniczone ThreadPoolExecutorpodobnego ThreadPoolExecutor(0, maximumPoolSize, 60L, TimeUnit.SECONDS, SynchronousQueue())?
Abhijit Sarkar
47

Jedno FixedThreadPooli CachedThreadPooldrugie jest złe w mocno obciążonych aplikacjach.

CachedThreadPool jest bardziej niebezpieczne niż FixedThreadPool

Jeśli Twoja aplikacja jest mocno obciążona i wymaga małych opóźnień, lepiej pozbyć się obu opcji ze względu na poniższe wady

  1. Nieograniczony charakter kolejki zadań: może powodować brak pamięci lub duże opóźnienia
  2. Długo działające wątki będą powodować CachedThreadPoolutratę kontroli podczas tworzenia wątków

Ponieważ wiesz, że jedno i drugie jest złem, mniejsze zło nie przynosi żadnego dobra. Preferuj ThreadPoolExecutor , który zapewnia szczegółową kontrolę wielu parametrów.

  1. Ustaw kolejkę zadań jako kolejkę ograniczoną, aby mieć lepszą kontrolę
  2. Have right RejectionHandler - Twoja własna obsługa RejectionHandler lub Default dostarczona przez JDK
  3. Jeśli masz coś do zrobienia przed / po zakończeniu zadania, zastąp beforeExecute(Thread, Runnable)iafterExecute(Runnable, Throwable)
  4. Zastąp ThreadFactory , jeśli jest wymagane dostosowanie wątku
  5. Kontroluj rozmiar puli wątków dynamicznie w czasie wykonywania (powiązane pytanie SE: Dynamiczna pula wątków )
Ravindra babu
źródło
A jeśli ktoś zdecyduje się skorzystać z commonPool?
Crosk Cool
1
@Ravindra - Pięknie wyjaśniłeś wady zarówno CachedThreadPool, jak i FixedThreadPool. To pokazuje, że masz głębokie zrozumienie pakietu współbieżności.
Ayaskant,
5

Mam więc program, który generuje wątki (~ 5-150), które wykonują wiele zadań.

Czy na pewno rozumiesz, w jaki sposób wątki są faktycznie przetwarzane przez wybrany system operacyjny i sprzęt? Jak Java mapuje wątki na wątki systemu operacyjnego, jak mapuje wątki na wątki procesora itp.? Pytam, ponieważ tworzenie 150 wątków w JEDNYM JRE ma sens tylko wtedy, gdy masz pod spodem masywne rdzenie / wątki procesora, co najprawdopodobniej nie ma miejsca. W zależności od używanego systemu operacyjnego i pamięci RAM utworzenie więcej niż n wątków może nawet spowodować zamknięcie środowiska JRE z powodu błędów OOM. Więc naprawdę powinieneś odróżnić wątki i pracę do wykonania przez te wątki, ile pracy jesteś w stanie przetworzyć itp.

I to jest problem z CachedThreadPool: nie ma sensu umieszczać w kolejce długich prac w wątkach, które w rzeczywistości nie mogą działać, ponieważ masz tylko 2 rdzenie procesora zdolne do przetwarzania tych wątków. Jeśli skończysz z 150 zaplanowanymi wątkami, możesz stworzyć wiele niepotrzebnych narzutów dla harmonogramów używanych w Javie i systemie operacyjnym do ich jednoczesnego przetwarzania. Jest to po prostu niemożliwe, jeśli masz tylko 2 rdzenie procesora, chyba że twoje wątki przez cały czas czekają na operacje we / wy lub takie. Ale nawet w takim przypadku wiele wątków spowodowałoby wiele operacji we / wy ...

I ten problem nie występuje w przypadku FixedThreadPool, utworzonej z np. 2 + n wątków, gdzie n jest oczywiście rozsądnie niskie, ponieważ przy tym sprzęcie i zasobach systemu operacyjnego są używane ze znacznie mniejszym narzutem na zarządzanie wątkami, które i tak nie mogą działać.

Thorsten Schöning
źródło
Czasami nie ma lepszego wyboru, możesz mieć tylko 1 rdzeń procesora, ale jeśli używasz serwera, na którym każde żądanie użytkownika wyzwoli wątek do przetworzenia żądania, nie będzie innego rozsądnego wyboru, szczególnie jeśli planujesz skalowanie serwera po powiększeniu bazy użytkowników.
Michel Feinstein,
@mFeinstein Jak można nie mieć wyboru, jeśli jest się w stanie wybrać implementację puli wątków? W twoim przykładzie z jednym rdzeniem procesora, który tworzy tylko więcej wątków, po prostu nie ma sensu, pasuje idealnie do mojego przykładu przy użyciu FixedThreadPool. To również łatwo się skaluje, najpierw z jednym lub dwoma wątkami roboczymi, później z 10 lub 15 w zależności od liczby rdzeni.
Thorsten Schöning
2
Zdecydowana większość implementacji serwerów WWW utworzy jeden nowy wątek dla każdego nowego żądania HTTP ... Nie będą się przejmować tym, ile rzeczywistych rdzeni ma maszyna, dzięki temu implementacja jest prostsza i łatwiejsza do skalowania. Dotyczy to wielu innych projektów, w których chcesz tylko raz zakodować i wdrożyć, a nie musisz ponownie kompilować i wdrażać, jeśli zmienisz maszynę, która może być instancją w chmurze.
Michel Feinstein
@mFeinstein Większość serwerów internetowych korzysta z pul wątków dla żądań samodzielnie, po prostu dlatego, że tworzenie wątków, które nie mogą działać, nie ma sensu lub używają pętli zdarzeń do połączeń, a następnie przetwarzają żądania w pulach lub tak dalej. Dodatkowo brakuje Ci sedna, który polega na tym, że pytanie dotyczy możliwości wybrania właściwej puli wątków i tworzenia wątków, które i tak nie mogą działać, nadal nie ma sensu. Stała pula wątków skonfigurowana do rozsądnej liczby wątków na maszynę w zależności od skalowania rdzeni.
Thorsten Schöning
3
@ ThorstenSchöning, posiadanie 50 wątków związanych z procesorem na maszynie 2-rdzeniowej nie jest pomocne. Posiadanie 50 wątków związanych z IO na maszynie z 2 rdzeniami może być bardzo pomocne.
Paul Draper