В чем разница между Task.Run() и Task.Factory.StartNew()

У меня есть метод:

private static void Method()
{
    Console.WriteLine("Method() started");

    for (var i = 0; i < 20; i++)
    {
        Console.WriteLine("Method() Counter = " + i);
        Thread.Sleep(500);
    }

    Console.WriteLine("Method() finished");
}

И я хочу запустить этот метод в новой Задаче. Я могу начать новую задачу следующим образом

var task = Task.Factory.StartNew(new Action(Method));

или

var task = Task.Run(new Action(Method));

Но есть ли разница между Task.Run() и Task.Factory.StartNew(). Оба они используют ThreadPool и запускают метод() сразу после создания экземпляра задачи. Когда мы должны использовать первый вариант и второй?

Ответ 1

Второй метод Task.Run был представлен в более поздней версии.NET Framework (в.NET 4.5).

Однако первый метод Task.Factory.StartNew дает вам возможность определить много полезных вещей о потоке, который вы хотите создать, в то время как Task.Run не предоставляет этого.

Например, скажем, что вы хотите создать длинный поток задач. Если для этой задачи будет использоваться поток пула потоков, то это можно считать злоупотреблением пулом потоков.

Одна вещь, которую вы могли бы сделать, чтобы избежать этого, - это запустить задачу в отдельном потоке. Недавно созданный поток, который будет посвящен этой задаче и будет уничтожен, как только ваша задача будет завершена. Вы не можете достичь этого с помощью Task.Run, в то время как вы можете сделать это с помощью Task.Factory.StartNew, как Task.Factory.StartNew ниже:

Task.Factory.StartNew(..., TaskCreationOptions.LongRunning);

Как сказано здесь:

Итак, в.NET Framework 4.5 Developer Preview, weve представила новый метод Task.Run. Это никоим образом не устаревает Task.Factory.StartNew, а скорее просто следует рассматривать как быстрый способ использования Task.Factory.StartNew без необходимости указывать набор параметров. Его ярлык. Фактически Task.Run фактически реализуется с точки зрения той же логики, что и для Task.Factory.StartNew, просто передавая некоторые параметры по умолчанию. Когда вы передаете действие в задачу. Run:

Task.Run(someAction);

что в точности эквивалентно:

Task.Factory.StartNew(someAction, 
    CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

Ответ 2

См. Эту статью в блоге, в которой описывается разница. В основном делаю:

Task.Run(A)

То же самое, что и делать:

Task.Factory.StartNew(A, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);   

Ответ 3

Task.Run был представлен в более новой версии .NET Framework, и это рекомендуется.

Начиная с .NET Framework 4.5, метод Task.Run является рекомендуемым способом запуска задачи, связанной с вычислениями. Используйте метод StartNew только тогда, когда вам требуется детальное управление для длительной задачи, связанной с вычислениями.

Task.Factory.StartNew имеет больше опций, Task.Run - это сокращение:

Метод Run предоставляет набор перегрузок, облегчающих запуск задачи с использованием значений по умолчанию. Это легкая альтернатива перегрузкам StartNew.

Под сокращением я подразумеваю технический ярлык:

public static Task Run(Action action)
{
    return Task.InternalStartNew(null, action, null, default(CancellationToken), TaskScheduler.Default,
        TaskCreationOptions.DenyChildAttach, InternalTaskOptions.None, ref stackMark);
}

Ответ 4

Согласно этому сообщению Стивена Клири, Task.Factory.StartNew() опасен:

Я вижу много кода в блогах и в SO вопросах, которые используют Task.Factory.StartNew для ускорения работы в фоновом потоке. У Стивена Тауба есть отличная статья в блоге, которая объясняет, почему Task.Run лучше, чем Task.Factory.StartNew, но я думаю, что многие просто не читают его (или не понимают). Итак, я взял те же аргументы, добавил несколько более убедительных формулировок и хорошо посмотрим, как это происходит. :) StartNew предлагает гораздо больше опций, чем Task.Run, но это довольно опасно, как видите. Вы должны предпочесть Task.Run, а не Task.Factory.StartNew в асинхронном коде.

Вот реальные причины:

  1. Не понимает асинхронных делегатов. На самом деле это то же самое, что и пункт 1 по причинам, по которым вы хотели бы использовать StartNew. Проблема заключается в том, что при передаче асинхронного делегата в StartNew естественно предположить, что возвращаемая задача представляет этот делегат. Однако, поскольку StartNew не понимает асинхронные делегаты, то, что фактически представляет эта задача, является только началом этого делегата. Это одна из первых ловушек, с которой сталкиваются кодеры при использовании StartNew в асинхронном коде.
  2. Запутанный планировщик по умолчанию. Хорошо, время для подвоха: в каком коде выполняется метод "А"?
Task.Factory.StartNew(A);

private static void A() { }

Ну, вы знаете, это вопрос с подвохом, а? Если вы ответили "поток пула потоков", извините, но это не правильно. "A" будет запускаться на любом выполняемом TaskScheduler!

Таким образом, это означает, что он может потенциально выполняться в потоке пользовательского интерфейса, если операция завершена, и он маршалирует обратно в поток пользовательского интерфейса из-за продолжения, как более подробно объясняет Стивен Клири в своем посте.

В моем случае я пытался запускать задачи в фоновом режиме при загрузке сетки данных для представления, а также при отображении занятой анимации. Task.Factory.StartNew() анимация не отображается при использовании Task.Factory.StartNew() но анимация отображается правильно, когда я переключился на Task.Run().

Для получения дополнительной информации, пожалуйста, смотрите https://blog.stephencleary.com/2013/08/startnew-is-dangerous.html

Ответ 5

Люди уже упоминали, что

Task.Run(A);

Эквивалентно

Task.Factory.StartNew(A, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

Но никто не упомянул, что

Task.Factory.StartNew(A);

Эквивалентно:

Task.Factory.StartNew(A, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Current);

Как вы можете видеть, два параметра для Task.Run и Task.Factory.StartNew:

  1. TaskCreationOptions - Task.Run использует TaskCreationOptions.DenyChildAttach что означает, что дочерние задачи не могут быть присоединены к родителю, TaskCreationOptions.DenyChildAttach это:

    var parentTask = Task.Run(() =>
    {
        var childTask = new Task(() =>
        {
            Thread.Sleep(10000);
            Console.WriteLine("Child task finished.");
        }, TaskCreationOptions.AttachedToParent);
        childTask.Start();
    
        Console.WriteLine("Parent task finished.");
    });
    
    parentTask.Wait();
    Console.WriteLine("Main thread finished.");
    

    Когда мы вызываем parentTask.Wait(), childTask не будет childTask, даже несмотря на то, что мы указали для него TaskCreationOptions.AttachedToParent, это потому, что TaskCreationOptions.DenyChildAttach запрещает детям присоединяться к нему. Если вы запустите тот же код с Task.Factory.StartNew вместо Task.Run, parentTask.Wait() будет ожидать childTask потому что Task.Factory.StartNew использует TaskCreationOptions.None

  2. TaskScheduler - Task.Run использует TaskScheduler.Default что означает, что для запуска задач всегда будет использоваться планировщик задач по умолчанию (тот, который запускает задачи в пуле потоков). Task.Factory.StartNew с другой стороны, использует TaskScheduler.Current что означает планировщик текущего потока, это может быть TaskScheduler.Default но не всегда. Фактически, при разработке приложений Winforms или WPF требуется обновить пользовательский интерфейс из текущего потока, для этого люди используют TaskScheduler.FromCurrentSynchronizationContext() задач TaskScheduler.FromCurrentSynchronizationContext(), если вы непреднамеренно создаете другую долгосрочную задачу внутри задачи, в которой используется TaskScheduler.FromCurrentSynchronizationContext() пользовательский интерфейс будет заморожен. Более подробное объяснение этого можно найти здесь

    Поэтому, как правило, если вы не используете задачу с вложенными детьми и всегда хотите, чтобы ваши задачи выполнялись в Пуле потоков, лучше использовать Task.Run, если у вас нет более сложных сценариев.

Ответ 6

Task.Factory.StartNew разницу между Task.Factory.StartNew и Task.Run, я Task.Factory.StartNew Task.Run:

  • В .NET Framework (включая 4.7) Task.Run - это метод расширения, который на самом деле является оберткой над Task.Factory.StartNew(action, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default). См. Справочный исходный код Microsoft.

    public static Task Run(Action action)
    {
        return Run(action, CancellationToken.None);
    }
    
    public static Task Run(Action action, CancellationToken cancellationToken)
    {
        return Task.Factory.StartNew(action, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default);
    }
    
  • В .NET Core Task.Run вызывает внутренний метод Task.InternalStartNew, который планирует и запускает новую задачу. Так что это больше не оболочка Task.StartNew метода Task.StartNew. См. Исходный код .NET Core:

    public static Task Run(Action action)
    {
        return Task.InternalStartNew(null, action, null, default, TaskScheduler.Default,
            TaskCreationOptions.DenyChildAttach, InternalTaskOptions.None);
    }
    
  • В то же время StartNew вызывает тот же метод Task.InternalStartNew. См. Исходный код .NET Core:

    public Task StartNew(Action action)
    {
        Task currTask = Task.InternalCurrent;
        return Task.InternalStartNew(currTask, action, null, m_defaultCancellationToken, GetDefaultScheduler(currTask),
            m_defaultCreationOptions, InternalTaskOptions.None);
    }
    
  • Таким образом, оба метода вызывают один и тот же внутренний метод (InternalStartNew) с разными параметрами:

    internal static Task InternalStartNew(
        Task creatingTask, Delegate action, object state, CancellationToken cancellationToken, TaskScheduler scheduler,
        TaskCreationOptions options, InternalTaskOptions internalOptions)
    {
        // Validate arguments.
        if (scheduler == null)
        {
            ThrowHelper.ThrowArgumentNullException(ExceptionArgument.scheduler);
        }
    
        // Create and schedule the task. This throws an InvalidOperationException if already shut down.
        // Here we add the InternalTaskOptions.QueuedByRuntime to the internalOptions, so that TaskConstructorCore can skip the cancellation token registration
        Task t = new Task(action, state, creatingTask, cancellationToken, options, internalOptions | InternalTaskOptions.QueuedByRuntime, scheduler);
    
        t.ScheduleAndStart(false);
        return t;
    }
    

Ответ 7

В моем приложении, которое вызывает две службы, я сравнивал как Task.Run, так и Task.Factory.StartNew. Я обнаружил, что в моем случае оба они работают нормально. Однако второй быстрее.