Кога да използвате Task.WaitAll срещу Task.WhenAll в .NET

TPL (Task Parallel Library) е една от най-интересните нови функции, добавени в последните версии на .NET framework. Методите Task.WaitAll и Task.WhenAll са два важни и често използвани метода в TPL.

Task.WaitAll блокира текущата нишка, докато всички други задачи завършат изпълнението. Методът Task.WhenAll се използва за създаване на задача, която ще завърши, ако и само ако всички останали задачи са завършени.

Така че, ако използвате Task.WhenAll, ще получите обект на задача, който не е завършен. Той обаче няма да блокира, но ще позволи на програмата да се изпълни. Напротив, извикването на метода Task.WaitAll всъщност блокира и чака всички други задачи да приключат.

По същество Task.WhenAll ще ви даде задача, която не е завършена, но можете да използвате ContinueWith веднага след като посочените задачи приключат изпълнението си. Обърнете внимание, че нито Task.WhenAll, нито Task.WaitAll действително ще изпълняват задачите; т.е. не се стартират задачи по тези методи. Ето как се използва ContinueWith с Task.WhenAll: 

Task.WhenAll (taskList) .ContinueWith (t => {

  // напишете кода си тук

});

Както се посочва в документацията на Microsoft, Task.WhenAll „създава задача, която ще завърши, когато всички обекти на Task в една безбройна колекция са изпълнени.“

Task.WhenAll срещу Task.WaitAll

Позволете ми да обясня разликата между тези два метода с прост пример. Да предположим, че имате задача, която изпълнява някаква дейност с потребителския интерфейс - да речем, някаква анимация трябва да бъде показана в потребителския интерфейс. Сега, ако използвате Task.WaitAll, потребителският интерфейс ще бъде блокиран и няма да бъде актуализиран, докато всички свързани задачи не бъдат завършени и блокът не бъде освободен. Ако обаче използвате Task.WhenAll в същото приложение, нишката на потребителския интерфейс няма да бъде блокирана и ще бъде актуализирана както обикновено.

И така, кой от тези методи трябва да използвате кога? Е, можете да използвате WaitAll, когато намерението синхронно се блокира, за да получите резултатите. Но когато искате да използвате асинхронност, бихте искали да използвате варианта WhenAll. Можете да изчакате Task.WhenAll, без да се налага да блокирате текущата нишка. Следователно може да искате да използвате await с Task.WhenAll вътре в асинхронен метод.

Докато Task.WaitAll блокира текущата нишка, докато всички чакащи задачи бъдат завършени, Task.WhenAll връща обект на задача. Task.WaitAll изхвърля AggregateException, когато една или повече от задачите хвърлят изключение. Когато една или повече задачи хвърлят изключение и изчаквате метода Task.WhenAll, той разгръща AggregateException и връща само първата.

Избягвайте да използвате Task.Run в цикли

Можете да използвате задачи, когато искате да изпълнявате едновременни дейности. Ако имате нужда от висока степен на паралелизъм, задачите никога не са добър избор. Винаги е препоръчително да избягвате използването на нишки на пула от нишки в ASP.Net. Следователно трябва да се въздържате от използване на Task.Run или Task.factory.StartNew в ASP.Net.

Task.Run винаги трябва да се използва за код, свързан с процесора. Task.Run не е добър избор в приложения на ASP.Net или приложения, които използват времето за изпълнение на ASP.Net, тъй като просто разтоварват работата в нишка на ThreadPool. Ако използвате ASP.Net Web API, заявката вече ще използва нишка ThreadPool. Следователно, ако използвате Task.Run във вашето приложение ASP.Net Web API, вие просто ограничавате мащабируемостта, като разтоварвате работата в друга работна нишка, без да има причина.

Имайте предвид, че има недостатък при използването на Task.Run в цикъл. Ако използвате метода Task.Run в цикъл, ще бъдат създадени множество задачи - по една за всяка единица работа или итерация. Ако обаче използвате Parallel.ForEach вместо използването на Task.Run вътре в цикъл, Partitioner се създава, за да избегне създаването на повече задачи за изпълнение на дейността, отколкото е необходима. Това може значително да подобри производителността, тъй като можете да избегнете твърде много превключватели на контекста и все пак да използвате множество ядра във вашата система.

Трябва да се отбележи, че Parallel.ForEach използва Partitioner вътрешно, за да разпределя колекцията в работни елементи. Между другото, това разпределение не се случва за всяка задача от списъка с елементи, а по-скоро се случва като партида. Това намалява включените режийни разходи и следователно подобрява производителността. С други думи, ако използвате Task.Run или Task.Factory.StartNew вътре в цикъл, те ще създадат нови задачи изрично за всяка итерация в цикъла. Parallel.ForEach е много по-ефективен, защото ще оптимизира изпълнението, като разпредели работното натоварване между множеството ядра във вашата система.