[C#] Wymuszenie zatrzymania grupy zadań


(CzoQś) #1

Witam,

Napisałem pewną aplikację wielowątkową, w której wykorzystałem tablicę Task jak poniżej:

var watki = new Task[x];


            for (int i = 0; i < x; i++)

            {

                watki[i] = Task.Factory.StartNew(Work, i);

            }


            Task.WaitAll(watki);

Do tego jak wynika z powyższego kodu istnieje pewna Metoda Work, która wykonuje pewne obliczenie...

Całość działa dosyć sprawnie i dobrze wykorzystuje moc procesorów wielordzeniowych.

Jednak chciałbym jeszcze dodać możliwość zatrzymania wszystkich zadań np. w momencie zamykania aplikacji, albo dedykowanym buttonem do tego..

Może ktoś mógłby doradzić jak sprawnie to zrobić..

Pozdrawiam


(Tomek Matz) #2

Czemu zdecydowałeś się na tablicę Task-ów, a nie Parallel.For? Pytam z ciekawości, bo z kodu wynika, że Parallel.For świetnie tutaj pasuje.

Jeśli chodzi o przerywanie Task-ów, to przykłady znajdują się na msdn. Słowo kluczowe CancellationToken.

http://msdn.microsoft.com/en-us/library/dd997396.aspx

http://msdn.microsoft.com/en-us/library/ee256691.aspx


(CzoQś) #3

Zaczynałem z ThreadPool (powyżej 64 wątków było trudniej blokować, żeby czekał na zakończenie wszystkich wątków), zmieniłem na Task (wydajność aplikacji taka sama jak ThreadPool a nie ma ograniczenia WaitAll), Parallel.For właśnie sprawdziłem i za każdym razem trochę wolniej działa niż w przypadku poprzedników…

Pokombinuję z tym “CancellationToken” - wydaje mi się że to chyba polega na wypluwaniu wyjątku więc pewnie całość trzeba w try…catch wziąć.


(Tomek Matz) #4

W przypadku Parallel.For może być lekki narzut spowodowany tym, że najpierw sprawdzane jest, czy lepiej puścić pętlę w osobnych wątkach, czy też w pojedynczym (CLR sam sobie to szacuje -> nie wiem jak). No ale za to masz mniej linijek kodu. Możesz powiedzieć jak duży spadek wydajności zaobserwowałeś? To nie powinny być duże różnice. I jeszcze jedno … tak wywołujesz tą pętlę?

int i = 0;

Parallel.For(i, length, index =>

{

//TODO: code!

});

Dokładnie tak.


(CzoQś) #5

Ustawiłem metodę Work tak aby obliczenia zajmowały kilkadziesiąt sekund…

Testy robione na dokładnie tych samych ustawieniach…

Task ~85s

Parallel.For ~110s

Przy okazji na podstawie: http://msdn.microsoft.com/en-us/library/ee256691.aspx

Parallel.ForEach ~113s


(Tomek Matz) #6

No nie ukrywam, że wyniki mnie zaskoczyły :stuck_out_tongue: To jest bardzo duża różnica. Jeśli kod tej funkcji Work nie jest tajemnicą, to jak możesz to wrzuć go potem.


(CzoQś) #7

No nie będę tu chwalić się swoim kodem bo jakoś bardzo wyszukany to nie jest :stuck_out_tongue:

Powiem tyle, że to gra logiczna z moim pomysłem wielowątkowego przeszukiwania z wykorzystaniem algorytmu minimax (czyli rekurencja z n-ruchami do przodu).

Test polegał na ustawieniu sporej liczy ruchów do przodu i sprawdzeniu ile czasu zajęły obliczenia. :slight_smile:


(somekind) #8

Więcej niż 64 wątki? Ta gra będzie wymagała serwera rackowego do działania?


(CzoQś) #9

W grach logicznych gdzie jest plansza NxN jest przeważnie kilkadziesiąt możliwych ruchów do wyboru…

Tu akurat max około 120…

Algorytm zakłada sprawdzenie każdego z takich ruchów i wybranie tego który zwróci najlepszą wartość - więc czemu nie wykorzystać do tego wielowątkowości?

Wystarczy że zrobimy pulę wątków gdzie pojedynczy wątek oznacza dokładnie jeden możliwy ruch i już jesteśmy w stanie zwrócić ten najlepszy ruch o wiele szybciej tym bardziej, że aktualnie tendencje są w ilość rdzeni a nie mhz. Innym ciekawym rozwiązaniem może być wykorzystanie opencl, ale to już trudniejsze ze względu na brak obsługi rekurencji - tak mi się wydaje…

Serwer nie jest potrzebny - wystarczy odpowiednio dobrać głębokość przeszukiwania aby osiągnąć czasy około 1-5s na zwrócenie ruchu.


(Fiołek) #10

somekind owi chodziło o to, że odpalanie więcej wątków niż jest rdzeni procesora tylko spowolni wykonywanie(skakanie pomiędzy wątkami na jednym rdzeniu jest dość kosztowne).

Domyślna implementacja _Task_a wewnętrznie używa ThreadPool, więc różnic w wydajności być nie powinno.


(Tomek Matz) #11

Tyle, że akurat 64 wątki to wcale nie tak dużo (powiedziałbym, że powyżej przeciętnej patrząc po procesach, które mam uruchomione u siebie na komputerze :slight_smile: ). Otwórzcie sobie chociażby menedżer zadań i zobaczcie ile wątków jednocześnie ma uruchomiony taki explorer.exe lub Wasza przeglądarka internetowa (zarówno w przypadku jednego jak i drugiego otwórzcie kilka okien).


(somekind) #12

Wykorzystać, ale upewnić się, czy korzysta się prawidłowo.

A co te wątki robią? Nic.

Jak sądzę, wątki w grze chyba jednak mają coś robić. Nie ma sensu tworzyć więcej wątków liczących niż jest rdzeni w procesorze, jeśli każdy z nich zajmuje w całości jeden rdzeń. W takiej sytuacji dodawanie nowych wątków jedynie wydłuża czas obliczania pojedynczych zadań.

Lepiej wykonywać równolegle w wątkach tyle zadań, ile jest rdzeni (np. 4), a potem dać im do liczenia następne.


(Tomek Matz) #13

Nie wiem co one robią. Być może powinienem, ale nie wiem. A Ty na jakiej podstawie stwierdzasz, że nic nie robią? Załóżmy otworzyłeś w przeglądarce kilka stron internetowych, które co jakiś czas ajax-em w tle sprawdzają sobie, czy pojawiły się nowe wiadomości (tak działa np. twitter). Sądzisz, że to wszystko obsługiwane jest przez jeden wątek?

Ja na programowaniu gier się nie znam. W tym temacie wypowiedziałem się, bo tyczy się on przede wszystkim TPL, a skoro o nim mowa to sądzisz, że używanie tej biblioteki to błąd? Dla przykładu … użycie takiej konstrukcji jak Parallel.For utworzy taką liczbę wątków jaką CLR uzna za słuszną. To może być ilość znacznie większa niż ilość fizycznych rdzeni.


(somekind) #14

Obecnie mam niecały 1000 wątków, które zajmują CPU, nawet nie w 10%. To jest nic.

Czym innym jest odwoływanie się do jakiegoś zasobu raz na jakiś czas, a czym innym ciągła praca obciążająca procesor.

Ja z kolei nie odnosiłem się do TPL tylko podzieliłem się spostrzeżeniem odnośnie tworzenia większej liczby wątków niż rdzeni ma procesor podczas złożonych obliczeń. (Zakładam, że z tym mamy do czynienia w tym właśnie przypadku.)

Być może, nie wiem jak to jest zaimplementowane. Masz jakiś dokładny opis tego jak TPL zarządza wątkami?


(Fiołek) #15

Parallel używa TaskSchedulera, który domyślnie używa ThreadPool(źródło: http://msdn.microsoft.com/en-us/library/dd997402.aspx ). ThreadPool odpala tyle wątków, ile uzna za słuszne(cytując za MSDN:

) i na moim kompie(przeciętny, 4 letni komp) to trochę ponad 32 tysiące. Z tym, że ThreadPool nie używa się do wątków, które mają długo działać(choć można odpalić _Task_a jako LongRunning) a raczej do modelu “fire and forget”. Jeśli to ma być operacja jednorazowa, która wykona się zanim system wywłaszczy wątek i odpali następny to ok, z tego co wiem narzut nie jest straszny, ale jeśli jest inaczej to lepiej ręcznie odpalić wątek, który ma małe szanse na wywłaszczenie przez system(w obrębie aplikacji, bo nie jesteśmy w stanie kontrolować innych aplikacji).