[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 Taska 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ć Taska 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).