различное поведение между Factory.StartNew и Task.Run?

Я пытаюсь понять разницу между Factory.StartNew и Task.Run. Я видел эквивалентность в разных местах, таких как здесь< /а>.

Я думаю, что мне нужно использовать Factory.StartNew() в моем случае, так как я хочу подключить свой собственный TaskScheduler.

Итак, подводя итог, кажется, что:

Task.Run(action)

Строго эквивалентно:

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

Но я провел несколько тестов с простым SerialQueue, взятым из образцов Microsoft для параллельного программирования с .NET Framework< /а>.

Вот простой код:

/// <summary>Represents a queue of tasks to be started and executed serially.</summary>
public class SerialTaskQueue
{
    /// <summary>The ordered queue of tasks to be executed. Also serves as a lock protecting all shared state.</summary>
    private Queue<object> _tasks = new Queue<object>();
    /// <summary>The task currently executing, or null if there is none.</summary>
    private Task _taskInFlight;

    /// <summary>Enqueues the task to be processed serially and in order.</summary>
    /// <param name="taskGenerator">The function that generates a non-started task.</param>
    public void Enqueue(Func<Task> taskGenerator) { EnqueueInternal(taskGenerator); }

    /// <summary>Enqueues the task to be processed serially and in order.</summary>
    /// <param name="taskOrFunction">The task or functino that generates a task.</param>
    /// <remarks>The task must not be started and must only be started by this instance.</remarks>
    private void EnqueueInternal(object taskOrFunction)
    {
        // Validate the task
        if (taskOrFunction == null) throw new ArgumentNullException("task");
        lock (_tasks)
        {
            // If there is currently no task in flight, we'll start this one
            if (_taskInFlight == null) StartTask_CallUnderLock(taskOrFunction);
            // Otherwise, just queue the task to be started later
            else _tasks.Enqueue(taskOrFunction);
        }
    }

    /// <summary>Starts the provided task (or function that returns a task).</summary>
    /// <param name="nextItem">The next task or function that returns a task.</param>
    private void StartTask_CallUnderLock(object nextItem)
    {
        Task next = nextItem as Task;
        if (next == null) next = ((Func<Task>)nextItem)();

        if (next.Status == TaskStatus.Created) next.Start();

        _taskInFlight = next;
        next.ContinueWith(OnTaskCompletion);
    }


    /// <summary>Called when a Task completes to potentially start the next in the queue.</summary>
    /// <param name="ignored">The task that completed.</param>
    private void OnTaskCompletion(Task ignored)
    {
        lock (_tasks)
        {
            // The task completed, so nothing is currently in flight.
            // If there are any tasks in the queue, start the next one.
            _taskInFlight = null;
            if (_tasks.Count > 0) StartTask_CallUnderLock(_tasks.Dequeue());
        }
    }
}

А теперь вот мой код какой-то смоделированной составной задачи (включая ожидание/продолжение).

    public static async Task SimulateTaskSequence(int taskId)
    {
        Console.WriteLine("Task{0} - Start working 1sec (ManagedThreadId={1} IsThreadPoolThread={2})", taskId, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
        Thread.Sleep(200);

        Console.WriteLine("Task{0} - Zzz 1st 1sec (ManagedThreadId={1} IsThreadPoolThread={2})", taskId, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
        await Task.Delay(200);

        Console.WriteLine("Task{0} - Done (ManagedThreadId={1} IsThreadPoolThread={2})", taskId, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
    }

Test1: использование очереди с Task.Run():

static void Main(string[] args)
{
    Console.WriteLine($"Starting test program (ManagedThreadId={Thread.CurrentThread.ManagedThreadId} IsThreadPoolThread={Thread.CurrentThread.IsThreadPoolThread})");

    SerialTaskQueue co_pQueue = new SerialTaskQueue();

    for (int i = 0; i < 2; i++)
    {
        var local = i;
        co_pQueue.Enqueue(() => Task.Run(() => { return SimulateTaskSequence(local); }));
    }
}

И результат правильный, очередь обрабатывается в ожидаемом порядке (достигните Task0 перед переключением на Task1).

Запуск тестовой программы (ManagedThreadId=1 IsThreadPoolThread=False)
Task0 – начало работы 1 сек (ManagedThreadId=5 IsThreadPoolThread=True)
Task0 – Zzz 1st 1 сек (ManagedThreadId=5 IsThreadPoolThread=True)
Task0 – выполнено ( ManagedThreadId=5 IsThreadPoolThread=True)
Task1 – начало работы через 1 секунду (ManagedThreadId=5 IsThreadPoolThread=True)
Task1 – Zzz 1st 1 sec (ManagedThreadId=5 IsThreadPoolThread=True)
Task1 – выполнено (ManagedThreadId=8 IsThreadPoolThread) = Верно)

Тест 2: простое использование Factory.StartNew с его идеальной эквивалентностью:

static void Main(string[] args)
{
    Console.WriteLine($"Starting test program (ManagedThreadId={Thread.CurrentThread.ManagedThreadId} IsThreadPoolThread={Thread.CurrentThread.IsThreadPoolThread})");

    SerialTaskQueue co_pQueue = new SerialTaskQueue();

    for (int i = 0; i < 2; i++)
    {
        var local = i;
        co_pQueue.Enqueue(() => Task.Factory.StartNew(() => { return SimulateTaskSequence(local); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default));
    }
}

Но на этот раз я получаю следующий вывод:

Запуск тестовой программы (ManagedThreadId=1 IsThreadPoolThread=False)
Task0 — Начало работы 1 сек (ManagedThreadId=5 IsThreadPoolThread=True)
Task0 – Zzz 1st 1 сек (ManagedThreadId=5 IsThreadPoolThread=True)
Task1 – Начало работы 1 сек (ManagedThreadId=5 IsThreadPoolThread=True) ЧТО?
Task1 – Zzz 1st 1 сек (ManagedThreadId=5 IsThreadPoolThread=True)
Task0 – выполнено (ManagedThreadId=9 IsThreadPoolThread=True)
Задача 1 — выполнено (ManagedThreadId=5 IsThreadPoolThread=True)

Я не понимаю разницы. Почему поведение отличается? Я думал, что это равнозначно?! (помните, шаг после подключения моего собственного планировщика)


person chrisdot    schedule 23.11.2017    source источник


Ответы (1)


Тип возврата фабрики задач — Task <Task>, тип возврата Task.Run — просто Task.

Вам нужно развернуть внутреннюю задачу с фабрикой, чтобы ваш ConinueWith в коде очереди запускал продолжение внутренней задачи, а не внешней задачи.

static void Main(string[] args)
{
    Console.WriteLine($"Starting test program (ManagedThreadId={Thread.CurrentThread.ManagedThreadId} IsThreadPoolThread={Thread.CurrentThread.IsThreadPoolThread})");

    SerialTaskQueue co_pQueue = new SerialTaskQueue();

    for (int i = 0; i < 2; i++)
    {
        var local = i;
        co_pQueue.Enqueue(() => Task.Factory.StartNew(() => { return SimulateTaskSequence(local); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap());
    }
}

Task.Run имеет перегрузку, которая принимает Func<Task>, которая делает это за вас. Если вы объявили делегата в Task.Run как Func<object>, вы увидите такое же поведение в Task.Run.

person Scott Chamberlain    schedule 23.11.2017
comment
Кстати: Есть ли причина, по которой StartNew не поддерживает Func‹Task› по дизайну? - person chrisdot; 27.11.2017
comment
@Christophe Потому что возможность развернуть задачу была необходима только потому, что async/await был добавлен в .net, а StartNew старше. - person Scott Chamberlain; 27.11.2017