В NET 4 Task.Factory.StartNew был основным методом планирования новой задачи. Многие перегрузки, предоставляемые высоко конфигурируемый механизм, который имеет гибкий набор параметров задаваемых в произвольном порядке, которые позволяют отменить и контролировать порядок выполнения. Обратной стороной гибкости является сложность. Вы должны знать, когда и какую перегрузку использовать, какой планировщик и тому подобное.
Так, в .NET Framework 4.5, была введен новый метод Task.Run. Это никоим образом не отменяет Task.Factory.StartNew, а должен рассматриваться просто как быстрый способ, для использования Task.Factory.StartNew без необходимости указания кучи параметров. Это ярлык. На самом деле, Task.Run фактически реализован по той же логике используемой для Task.Factory.StartNew, просто получил параметры по-умолчанию. Вы просто передаете Action в Task.Run:
Task.Run(someAction);
что будет эквивалентно:
Task.Factory.StartNew(someAction, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
Таким образом, Task.Run может и должен использоваться для наиболее распространенных случаев разгрузки некоторой работы, которые будут обработаны в ThreadPool. Это не значит, что Task.Factory.StartNew никогда не будет использоваться; это далеко не так. В Task.Factory.StartNew по-прежнему остается еще много важных (более продвинутый) возможностей. Вы можете контролировать TaskCreationOptions для понимания того, как задача будет вести себя, так же вы можете контролировать планировщик и т.д.
Task.Run обеспечивает восемь перегрузок, поддерживает все следующие комбинации:
- Task vs Task<TResult>
- Cancelable vs non-cancelable
- Synchronous vs asynchronous delegate
Первых два пункта должны быть очевидными. Для первого: есть перегрузки, которые возвращают Task (для операций, которые не имеют результата) и есть перегрузки, которые возвращают Task <TResult> (для операций, которые имеют результат типа TResult). Есть также перегрузки, которые принимают CancellationToken.
Третий пункт более интересный и напрямую связаны с поддержкой асинхронного языка в C # и Visual Basic. Остановимся на мгновение на Task.Factory.StartNew. Если я напишу следующий код:
var t = Task.Factory.StartNew(() => { Task inner =Task.Factory.StartNew(() => {}); return inner; });
тип «T» будет Task <Task>; делегат задачи типа Func <TResult>, TResult в данном случае является Task’ом, и, таким образом StartNew возвращается Task<Task>. Точно так же, если бы я изменил на это:
var t = Task.Factory.StartNew(() => { Task<int> inner = Task.Factory.StartNew(() => 42)); return inner; });
тип «T» теперь будет Task<Task<int>>. Делегат задачи является Func <TResult>, TResult теперь Task<int>, и, таким образом StartNew возвращает Task<Task<int>>. Почему это уместно? Рассмотрим теперь, что произойдет, если я напишу следующее:
var t = Task.Factory.StartNew(async delegate { await Task.Delay(1000); return 42; });
Используя ключевое слово async, компилятор собирает этот делегат в Func<Task<int>>: вызов вернет Task<int>. А так как делегат Func<Task<int>>, TResult является Task<int>, и, таким образом, тип «t» будет Task<Task<int>>, не Task<int>.
Чтобы обработать все эти случаи, в NET 4 был введен метод Unwrap. У Unwrap есть две перегрузки, обе из которых являются расширениями, один типа Task<Task>, второй Task <Task<TResult>>. Назвали этот метод Unwrap, потому что он, по сути, «разворачивает» внутреннюю задачу, которая вернулась в результате выполнения внешней. Вызов Unwrap на Task<Task> дает Вам новую задачу, которая представляет собой возможное завершение внутренней задачи. Точно так же, вызывая Unwrap на Task<Task<TResult >> Вы получите новый Task <TResult>, который представляет собой возможное завершение этой внутренней задачи. (В обоих случаях, если во внешней задаче возникла ошибка или она была отменена,то внутренней задачи не будет, так как нет никакого результата от задачи, которая не дошла до конца). Вернемся обратно к предыдущему примеру, если бы я хотел видеть ‘t’ возвращаемым результатом (в данном случае это значение 42), я мог бы написать:
var t = Task.Factory.StartNew(async delegate { await Task.Delay(1000); return 42; }).Unwrap();
Переменная ‘t’ теперь будет типа Task<int>, представляющего результат асинхронного вызова.
Приступим к рассмотру Task.Run. Поскольку ожидается, что этот функционал будет характерным для людей, которые хотят разгрузить работу в ThreadPool и для этого использовать async/await, было принято решение взять на вооружение Task.Run. У Task.Run есть перегрузки, которые принимают Action, Func <TResult>, Func <Task> и Func<Task<TResult>>. Внутренне, впринципе, Task.Run делает то же самое, что и Task.Factory.StartNew выше. Так что, когда я пишу:
var t = Task.Run(async delegate { await Task.Delay(1000); return 42; });
тип «t» является Task <int>, и реализация этой перегрузки Task.Run в основном эквивалентна:
var t = Task.Factory.StartNew(async delegate { await Task.Delay(1000); return 42; }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();
Как и упоминалось ранее — это всего лишь ярлык.
Приведем еще небольшой пример с использованием await:
int result = await Task.Run(async () => { await Task.Delay(1000); return 42; });
тип результата будет int и примерно через секунду после начала работа переменной result будет присвоено значение 42.
Интересно, что ключевое слово await можно рассматривать как почти эквивалентный методу Unwrap. Так, если мы вернемся назад к Task.Factory.StartNew, то я мог бы переписать последний фрагмент выше следующим образом (используя Unwrap):
int result = await Task.Factory.StartNew(async delegate { await Task.Delay(1000); return 42; }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();
или, вместо того чтобы использовать Unwrap, я мог бы использовать еще раз await:
int result = await await Task.Factory.StartNew(async delegate { await Task.Delay(1000); return 42; }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
await await — не опечатка. Task.Factory.StartNew возвращается Task<Task<int>>. Если мы дождемся выполнения Task<Task<int>> то получим Task<int>, и если и далее так продолжать, то дождясь Task <int> получим int.
Вот и вся разница между этими функционалами. Всем удачного программирования и не переусердствуйте с фоновыми задачами:)
Ссылка на источник: Task.Run vs Task.Factory.StartNew
Добавить комментарий