Kiedy używać puli wątków w języku C #? [Zamknięte]


127

Próbowałem nauczyć się programowania wielowątkowego w C # i nie wiem, kiedy najlepiej jest używać puli wątków, a kiedy tworzyć własne wątki. Jedna książka zaleca używanie puli wątków tylko do małych zadań (cokolwiek to oznacza), ale nie mogę znaleźć żadnych prawdziwych wskazówek. Jakie kwestie należy wziąć pod uwagę przy podejmowaniu tej decyzji programowej?

Odpowiedzi:


48

Jeśli masz wiele zadań logicznych, które wymagają ciągłego przetwarzania i chcesz, aby były wykonywane równolegle, użyj puli + harmonogramu.

Jeśli chcesz wykonywać zadania związane z we / wy jednocześnie, takie jak pobieranie rzeczy ze zdalnych serwerów lub dostępu do dysku, ale musisz to robić, powiedz co kilka minut, a następnie utwórz własne wątki i zabij je po zakończeniu.

Edycja: Jeśli chodzi o niektóre kwestie, używam pul wątków do uzyskiwania dostępu do bazy danych, fizyki / symulacji, sztucznej inteligencji (gry) i do zadań skryptowych uruchamianych na maszynach wirtualnych, które przetwarzają wiele zadań zdefiniowanych przez użytkownika.

Zwykle pula składa się z 2 wątków na procesor (więc obecnie prawdopodobnie 4), jednak możesz ustawić żądaną liczbę wątków, jeśli wiesz, ile potrzebujesz.

Edycja: Powodem tworzenia własnych wątków są zmiany kontekstu (to znaczy, gdy wątki muszą przełączać się w proces i poza nim, wraz z pamięcią). Posiadanie bezużytecznych zmian kontekstu, powiedzmy, kiedy nie używasz swoich wątków, po prostu pozostawienie ich w spokoju, jak można powiedzieć, może z łatwością zmniejszyć wydajność programu (powiedzmy, że masz 3 uśpione wątki i 2 aktywne wątki). Tak więc, jeśli te pobierające wątki tylko czekają, pochłaniają mnóstwo procesora i ochładzają pamięć podręczną dla twojej prawdziwej aplikacji


2
Ok, ale czy możesz wyjaśnić, dlaczego tak do tego podchodzisz? Na przykład, jakie są wady korzystania z puli wątków do pobierania ze zdalnych serwerów lub wykonywania operacji we / wy dysku?

8
Jeśli wątek oczekuje na obiekt synchronizacji (zdarzenie, semafor, mutex itp.), Wówczas wątek nie zużywa procesora.
Brannon

7
Jak powiedział Brannon, powszechnym mitem jest to, że tworzenie wielu wątków wpływa na wydajność. W rzeczywistości nieużywane wątki zużywają bardzo mało zasobów. Przełączniki kontekstowe zaczynają być problemem tylko w serwerach o bardzo wysokich wymaganiach (w tym przypadku, zobacz Porty zakończenia we / wy, aby uzyskać alternatywę).
FDCastel

12
Czy bezczynne wątki wpływają na wydajność? To zależy od tego, jak czekają. Jeśli są dobrze napisane i oczekują na obiekt synchronizacji, nie powinny zużywać zasobów procesora. Jeśli czekasz w pętli, która okresowo budzi się w celu sprawdzenia wyników, marnuje procesor. Jak zawsze, wszystko sprowadza się do dobrego kodowania.
Bill

2
Bezczynne zarządzane wątki zużywają pamięć dla swojego stosu. Domyślnie 1 MiB na wątek. Dlatego lepiej, aby wszystkie wątki działały.
Vadym Stetsiak

48

Sugerowałbym użycie puli wątków w C # z tych samych powodów, co w przypadku każdego innego języka.

Jeśli chcesz ograniczyć liczbę działających wątków lub nie chcesz, aby narzuty związane z ich tworzeniem i niszczeniem były możliwe, użyj puli wątków.

Przez małe zadania czytana książka oznacza zadania o krótkim czasie życia. Jeśli utworzenie wątku, który działa tylko przez jedną sekundę, zajmuje dziesięć sekund, jest to jedno miejsce, w którym powinieneś używać pul (zignoruj ​​moje rzeczywiste dane, liczy się stosunek).

W przeciwnym razie spędzasz większość czasu na tworzeniu i niszczeniu wątków, zamiast po prostu wykonywać pracę, do której są przeznaczone.


28

Oto ładne podsumowanie puli wątków w .Net: http://blogs.msdn.com/pedram/archive/2007/08/05/dedicated-thread-or-a-threadpool-thread.aspx

Post zawiera również kilka wskazówek, kiedy nie powinieneś używać puli wątków i zamiast tego rozpoczynać własny wątek.


8
-1 dla łącza. Jestem pewien, że to dobre łącze, ale oczekuję, że SO będzie samowystarczalny.
Jon Davis

26
@ stimpy77 - to złe oczekiwanie. SO nigdy nie może być samowystarczalne, ponieważ nie jest ostatecznym autorytetem we wszystkich pytaniach, ani też wszystkie szczegółowe informacje na każdy temat nie mogą (i powinny) być powielane w każdej odpowiedzi SO, która dotyczy tego tematu. (i nie sądzę, że masz nawet wystarczającą reputację, aby zignorować każdą odpowiedź Jona Skeeta, która ma link wychodzący, nie mówiąc już o wszystkich odpowiedziach od wszystkich użytkowników SO, którzy mają linki wychodzące :-))
Franci Penov

2
Być może byłem zbyt zwięzły, może powinienem to wyjaśnić. Nie jestem przeciwnikiem linków. Jestem przeciwko odpowiedziom, które zawierają tylko link. Nie sądzę, żeby to była odpowiedź. Teraz, gdyby opublikowano krótkie podsumowanie odpowiedzi, aby podsumować, w jaki sposób ma zastosowanie linkowana treść, byłoby to do przyjęcia. Poza tym przyjechałem tutaj, szukając odpowiedzi na ten sam problem i ta odpowiedź mnie irytowała, ponieważ był to kolejny link, na który musiałem kliknąć, aby mieć pojęcie, co może powiedzieć w odniesieniu do konkretnego problemu. W każdym razie, gdzie ma do tego Jon Skeet? I dlaczego powinno mnie to obchodzić?
Jon Davis,

8
„Przyszedłeś do tego postu dwa lata po opublikowaniu i wszystko, co tu skopiowałem, mogło być już nieaktualne”. Więc może link. Opublikuj zwięzłe, ale kompletne podsumowanie podczas publikowania linku, nigdy nie wiesz, czy link zgaśnie lub zepsuje się.
Jon Davis,

2
Nie zgadzam się ze stymulantem: nie ideą postów zawierających tony informacji z powodu niemożności wykonania, ani wzywania kogoś w tej sprawie. Powiedziałbym jednak, że bardziej prawdopodobne jest, że łącze przestanie działać, niż treść zostanie wycofana / usunięta. Tak więc więcej treści jest przyjemne, gdy pozwala na to okazja. Wszyscy jesteśmy (głównie) wolontariuszami, więc bądź wdzięczny za to, co dostajesz - dzięki Franci :)
zanlok

14

Gorąco polecam przeczytanie tego bezpłatnego e-booka: Threading in C # autorstwa Josepha Albahariego

Przeczytaj przynajmniej sekcję „Pierwsze kroki”. E-book stanowi świetne wprowadzenie i zawiera również wiele zaawansowanych informacji o wątkach.

Wiedza o tym, czy używać puli wątków, to dopiero początek. Następnie musisz określić, która metoda wprowadzania puli wątków najlepiej odpowiada Twoim potrzebom:

  • Biblioteka równoległa zadań (.NET Framework 4.0)
  • ThreadPool.QueueUserWorkItem
  • Asynchroniczni delegaci
  • BackgroundWorker

Ten e-book wyjaśnia to wszystko i radzi, kiedy ich używać, a kiedy tworzyć własny wątek.


8

Pula wątków została zaprojektowana w celu zmniejszenia przełączania kontekstów między wątkami. Rozważmy proces, w którym działa kilka komponentów. Każdy z tych komponentów może tworzyć wątki robocze. Im więcej wątków w procesie, tym więcej czasu marnuje się na przełączanie kontekstów.

Teraz, gdyby każdy z tych składników kolejkował elementy do puli wątków, miałbyś znacznie mniej narzutu przełączania kontekstu.

Pula wątków została zaprojektowana w celu maksymalizacji pracy wykonywanej na Twoich procesorach (lub rdzeniach procesora). Dlatego domyślnie pula wątków obraca wiele wątków na procesor.

Istnieją sytuacje, w których nie chcesz używać puli wątków. Jeśli czekasz na we / wy, czekasz na zdarzenie itp., Wiążesz ten wątek puli wątków i nie może być używany przez nikogo innego. Ta sama idea odnosi się do długotrwałych zadań, chociaż to, co stanowi długotrwałe zadanie, jest subiektywne.

Pax Diablo również ma rację. Rozwijanie wątków nie jest darmowe. Zajmuje to trochę czasu i zajmuje dodatkowe miejsce w stosie. Pula wątków ponownie wykorzysta wątki, aby zamortyzować ten koszt.

Uwaga: zapytałeś o użycie wątku puli wątków do pobierania danych lub wykonywania operacji we / wy dysku. Nie powinieneś używać do tego wątku puli wątków (z powodów, które opisałem powyżej). Zamiast tego użyj asynchronicznych operacji we / wy (znanych również jako metody BeginXX i EndXX). Na to FileStreambyłoby BeginReadi EndRead. Na to HttpWebRequestbyłoby BeginGetResponsei EndGetResponse. Są bardziej skomplikowane w użyciu, ale są właściwym sposobem wykonywania wielowątkowych operacji we / wy.


1
ThreadPool to sprytny automat. „Jeśli kolejka pozostaje nieruchoma przez ponad pół sekundy, odpowiada, tworząc więcej wątków - jeden na pół sekundy - aż do pojemności puli wątków” ( albahari.com/threading/#_Optimizing_the_Thread_Pool ). Również prawie asynchroniczne operacje z BeginXXX-EndXXX są używane przez ThreadPool. Więc normalne jest używanie ThreadPool do pobierania danych i często niejawnie używane.
Artru

6

Uważaj na pulę wątków .NET w przypadku operacji, które mogą blokować jakąkolwiek istotną, zmienną lub nieznaną część ich przetwarzania, ponieważ jest ona podatna na zanik wątków. Rozważ użycie równoległych rozszerzeń .NET, które zapewniają dużą liczbę logicznych abstrakcji w operacjach wątkowych. Obejmują one również nowy harmonogram, który powinien być ulepszeniem puli wątków. Zobacz tutaj


2
Odkryliśmy to na własnej skórze! ASP.Net używa Threadpool, więc nie mogliśmy używać go tak agresywnie, jak byśmy chcieli.
noocyte

3

Jednym z powodów używania puli wątków tylko do małych zadań jest ograniczona liczba wątków w puli wątków. Jeśli jeden jest używany przez długi czas, uniemożliwia użycie tego wątku przez inny kod. Jeśli zdarzy się to wiele razy, pula wątków może zostać wykorzystana.

Korzystanie z puli wątków może mieć subtelne efekty - na przykład niektóre liczniki czasu .NET używają wątków puli wątków i nie będą uruchamiane.


2

Jeśli masz zadanie w tle, które będzie działać przez długi czas, na przykład przez cały okres istnienia Twojej aplikacji, stworzenie własnego wątku jest rozsądną rzeczą. Jeśli masz krótkie zadania, które należy wykonać w wątku, użyj puli wątków.

W aplikacji, w której tworzysz wiele wątków, narzut związany z tworzeniem wątków staje się znaczny. Korzystanie z puli wątków tworzy wątki raz i używa ich ponownie, unikając w ten sposób narzutu tworzenia wątków.

W aplikacji, nad którą pracowałem, przejście od tworzenia wątków do korzystania z puli wątków dla wątków krótkotrwałych naprawdę pomogło w jej wdrożeniu.


Wyjaśnij, czy masz na myśli „pulę wątków” czy „pulę wątków”. To są bardzo różne rzeczy (przynajmniej w MS CLR).
bzlm

2

Aby uzyskać najwyższą wydajność z współbieżnie wykonywanymi jednostkami, napisz własną pulę wątków, w której pula obiektów Thread jest tworzona podczas uruchamiania i przejdź do blokowania (wcześniej zawieszonego), czekając na kontekst do uruchomienia (obiekt ze standardowym interfejsem zaimplementowanym przez Twój kod).

Tak wiele artykułów na temat zadań, wątków i puli wątków platformy .NET nie zawiera informacji niezbędnych do podjęcia decyzji o wydajności. Ale kiedy je porównasz, wygrywają wątki, a zwłaszcza pula wątków. Są najlepiej dystrybuowane między procesorami i uruchamiają się szybciej.

Należy omówić fakt, że główna jednostka wykonawcza systemu Windows (w tym Windows 10) jest wątkiem, a narzut przełączania kontekstu systemu operacyjnego jest zwykle pomijalny. Mówiąc najprościej, nie byłem w stanie znaleźć przekonujących dowodów na wiele z tych artykułów, niezależnie od tego, czy w artykule podano wyższą wydajność poprzez oszczędność przełączania kontekstu, czy lepsze wykorzystanie procesora.

Teraz trochę realizmu:

Większość z nas nie potrzebuje, aby nasza aplikacja była deterministyczna, a większość z nas nie ma tła z wątkami, co często wiąże się na przykład z tworzeniem systemu operacyjnego. To, co napisałem powyżej, nie jest dla początkującego.

Dlatego najważniejsze może być omówienie tego, co jest łatwe do zaprogramowania.

Jeśli utworzysz własną pulę wątków, będziesz mieć trochę do zrobienia, ponieważ będziesz musiał zająć się śledzeniem stanu wykonania, jak symulować zawieszenie i wznowienie oraz jak anulować wykonanie - w tym w całej aplikacji zamknąć. Być może będziesz musiał się również zastanowić, czy chcesz dynamicznie rozwijać swoją pulę, a także jakie ograniczenia pojemności będą miały Twoja pula. Mogę napisać taki framework w godzinę, ale to dlatego, że robiłem to wiele razy.

Być może najłatwiejszym sposobem napisania jednostki wykonawczej jest użycie zadania. Piękno zadania polega na tym, że możesz go utworzyć i uruchomić w kodzie (chociaż może być wymagana ostrożność). Możesz przekazać token anulowania do obsługi, gdy chcesz anulować zadanie. Ponadto stosuje podejście obiecujące do łączenia zdarzeń w łańcuchy i może zwrócić określony typ wartości. Co więcej, dzięki async i await istnieje więcej opcji, a Twój kod będzie bardziej przenośny.

W istocie ważne jest, aby zrozumieć zalety i wady funkcji Tasks, Threads i .NET ThreadPool. Jeśli potrzebuję wysokiej wydajności, zamierzam używać wątków i wolę używać własnej puli.

Łatwym sposobem porównania jest uruchomienie 512 wątków, 512 zadań i 512 wątków puli wątków. Na początku znajdziesz opóźnienie w Threads (stąd po co pisać pulę wątków), ale wszystkie 512 wątków będzie działać w ciągu kilku sekund, podczas gdy zadania i wątki .NET ThreadPool zajmują do kilku minut, aby rozpocząć.

Poniżej wyniki takiego testu (czterordzeniowy i5 z 16 GB RAM-u), dając każdemu 30 sekund na uruchomienie. Wykonywany kod wykonuje proste operacje we / wy pliku na dysku SSD.

Wyniki testów


1
FYI, zapomniałem wspomnieć, że zadania i wątki .NET są symulowaną współbieżnością w .NET, a zarządzanie odbywa się w .NET, a nie w systemie operacyjnym - ten ostatni jest znacznie bardziej wydajny w zarządzaniu współbieżnymi wykonaniami. Używam zadań do wielu rzeczy, ale używam wątku systemu operacyjnego w celu uzyskania dużej wydajności wykonywania. MS twierdzi, że zadania i wątki .NET są lepsze, ale ogólnie mają zrównoważyć współbieżność między aplikacjami .NET. Jednak aplikacja serwera działałaby najlepiej, gdyby system operacyjny obsługiwał współbieżność.

Chciałbym zobaczyć implementację Twojego niestandardowego Threadpool. Niezły napis!
Franciszek

Nie rozumiem Twoich wyników testu. Co oznacza „jednostki uruchomione”? Porównujesz 34 taksówki z 512 wątkami? Czy mógłbyś to wyjaśnić?
Elmue

Jednostka jest po prostu metodą do jednoczesnego wykonywania w wątku roboczym Task, Thread lub .NET ThreadPool, moim teście porównującym wydajność uruchamiania / uruchamiania. Każdy test ma 30 sekund na utworzenie 512 wątków od podstaw, 512 zadań, 512 wątków roboczych puli wątków lub wznowienie puli 512 uruchomionych wątków oczekujących na kontekst do wykonania. Wątki robocze Tasks i ThreadPool działają powoli, więc 30 sekund to za mało czasu, aby je wszystkie uruchomić. Jeśli jednak minimalna liczba wątków roboczych puli wątków jest najpierw ustawiona na 512, zarówno wątki robocze zadań, jak i wątków puli wątków będą działać prawie tak szybko, jak 512 wątków od podstaw.


1

Pule wątków są świetne, gdy masz więcej zadań do przetworzenia niż dostępnych wątków.

Możesz dodać wszystkie zadania do puli wątków i określić maksymalną liczbę wątków, które mogą być uruchomione w określonym czasie.

Sprawdź stronę w witrynie MSDN: http://msdn.microsoft.com/en-us/library/3dasc8as(VS.80).aspx


Ok, myślę, że to wiąże się z moim innym pytaniem. Skąd wiesz, ile masz dostępnych wątków w danym momencie?

Cóż, trudno powiedzieć. Będziesz musiał przeprowadzić testy wydajności. Po pewnym czasie dodanie większej liczby wątków nie zapewni większej szybkości. Dowiedz się, ile procesorów jest zainstalowanych na komputerze - to będzie dobry punkt wyjścia. Następnie idź w górę, jeśli prędkość przetwarzania nie poprawi się, nie dodawaj więcej wątków.
lajos

1

Jeśli możesz, zawsze używaj puli wątków, pracuj na najwyższym możliwym poziomie abstrakcji. Pule wątków ukrywają tworzenie i niszczenie wątków, zwykle jest to dobra rzecz!


1

W większości przypadków możesz korzystać z puli, ponieważ unikasz kosztownego procesu tworzenia wątku.

Jednak w niektórych scenariuszach możesz chcieć utworzyć wątek. Na przykład, jeśli nie jesteś jedyną osobą korzystającą z puli wątków, a utworzony wątek jest długotrwały (aby uniknąć zużywania zasobów udostępnionych) lub na przykład jeśli chcesz kontrolować rozmiar stosu wątku.


1

Nie zapomnij zbadać pracownika w tle.

Znajduję w wielu sytuacjach, daje mi to, czego chcę, bez podnoszenia ciężarów.

Twoje zdrowie.


Kiedy jest to prosta aplikacja, która działa i masz jeszcze jedno zadanie do wykonania, bardzo łatwo jest wykonać ten kod. nie podałeś jednak linków: specyfikacja i tutorial
zanlok

0

Zwykle używam Threadpool, gdy muszę po prostu zrobić coś w innym wątku i nie obchodzi mnie, kiedy działa lub kończy. Coś jak rejestrowanie, a może nawet pobieranie pliku w tle (chociaż są lepsze sposoby na zrobienie tego w stylu asynchronicznym). Używam własnego wątku, gdy potrzebuję większej kontroli. Odkryłem również, że używanie kolejki Threadsafe (zhakuj własną) do przechowywania "obiektów poleceń" jest przyjemne, gdy mam wiele poleceń, nad którymi muszę pracować w> 1 wątku. Możesz więc podzielić plik Xml i umieścić każdy element w kolejce, a następnie mieć wiele wątków pracujących nad przetwarzaniem tych elementów. Napisałem taką kolejkę już na uni (VB.net!), Że przekonwertowałem na C #. Zawarłem to poniżej bez konkretnego powodu (ten kod może zawierać błędy).

using System.Collections.Generic;
using System.Threading;

namespace ThreadSafeQueue {
    public class ThreadSafeQueue<T> {
        private Queue<T> _queue;

        public ThreadSafeQueue() {
            _queue = new Queue<T>();
        }

        public void EnqueueSafe(T item) {
            lock ( this ) {
                _queue.Enqueue(item);
                if ( _queue.Count >= 1 )
                    Monitor.Pulse(this);
            }
        }

        public T DequeueSafe() {
            lock ( this ) {
                while ( _queue.Count <= 0 )
                    Monitor.Wait(this);

                return this.DeEnqueueUnblock();

            }
        }

        private T DeEnqueueUnblock() {
            return _queue.Dequeue();
        }
    }
}

Niektóre problemy z tym podejściem: - Wywołania DequeueSafe () będą czekać, aż element zostanie wywołany EnqueuedSafe (). Rozważ użycie jednego z przeciążeń Monitor.Wait (), określając limit czasu. - Blokowanie tego nie jest zgodne z najlepszymi praktykami, a raczej utwórz pole obiektu tylko do odczytu. - Mimo że Monitor.Pulse () jest lekka, wywołanie jej, gdy kolejka zawiera tylko 1 element, byłoby bardziej wydajne. - DeEnqueueUnblock () powinno najlepiej sprawdzać queue.Count> 0 (wymagane, jeśli używane są Monitor.PulseAll lub limity czasu oczekiwania)
Craig Nicholson

0

Chciałem, aby pula wątków rozdzielała pracę między rdzeniami z jak najmniejszym opóźnieniem, a to nie musiało dobrze współgrać z innymi aplikacjami. Okazało się, że wydajność puli wątków .NET nie była tak dobra, jak mogłaby być. Wiedziałem, że chcę mieć jeden wątek na rdzeń, więc napisałem własną klasę zastępczą puli wątków. Kod jest dostarczany jako odpowiedź na inne pytanie StackOverflow tutaj .

Jeśli chodzi o pierwotne pytanie, pula wątków jest przydatna do dzielenia powtarzalnych obliczeń na części, które mogą być wykonywane równolegle (zakładając, że mogą być wykonywane równolegle bez zmiany wyniku). Ręczne zarządzanie wątkami jest przydatne w przypadku zadań takich jak interfejs użytkownika i we / wy.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.