Поведение планировщика задач .Net - PullRequest
0 голосов
/ 08 марта 2019

Я поиграл с задачами .Net, получив следующий код:

    public static async Task TaskSchedulerBehaviour()
    {
        var topLevelTasks = Enumerable.Range(0, 5).Select(async n =>
        {
            await Task.Delay(50); // THIS LINE MAKES THE DIFFERENCE
            var steps = Enumerable.Range(0, 100000);
            foreach (var batch in steps.Batch(1000)) { /* ".Batch" is contained in MoreLinq */
                await Task.WhenAll(batch.Select(async step => await WorkStep(n, step)));
            }
        });
        await Task.WhenAll(topLevelTasks);

        async Task WorkStep(int worker, int step)
        {
            if (step % 100 == 0) {
                Console.WriteLine($"worker={worker}, step={step}");
            }

            await Task.Delay(10);
        }
    }

Показанный код содержит несколько «больших» задач верхнего уровня, которые выполняют большую работу (= много небольших задач (WorkStep); которые вызывают только Task.Delay).

Одна строка в коде помечена комментарием: если эта строка удалена, может случиться так, что некоторые задачи верхнего уровня будут поставлены в очередь, пока все остальные не будут выполнены. Кажется, они могут голодать, если другие задачи «верхнего уровня» очень и очень интенсивны.

С другой стороны, если я добавлю закомментированную строку, поведение будет намного лучше: кажется, что все задачи верхнего уровня получают примерно одинаковое количество времени для выполнения своих дочерних задач. Они работают одновременно.

Почему это происходит? Разве планировщик задач не является простой FIFO-очередью или чем-то вроде этого?

Большое спасибо

Ответы [ 2 ]

3 голосов
/ 08 марта 2019

Предположим, вы говорите о планировщике задач пула потоков, который является одним из многих возможных планировщиков задач ...

Почему это происходит?Разве планировщик задач не является простой FIFO-очередью или чем-то вроде этого?

Существует одна общая очередь, которая обычно (не строго) FIFO, плюс каждый поток пула потоков имеет свой собственныйлокальная очередь, которая обычно (не строго) LIFO .Потоки потоков могут воровать из локальных очередей других потоков, если им нечего делать.

Кроме того, планировщики задач используются для выполнения только синхронного кода .Концепция async / await - это уровень абстракции над планировщиками задач.Таким образом, добавляя await Task.Delay, ваш код фактически разбивает одну концептуальную задачу async на несколько частей, каждая из которых ставится в очередь в пуле потоков в соответствующее время.Т.е. первая часть ставится в очередь сразу;когда он запускается, он вызывает Task.Delay (запуск таймера), а затем нажимает await, вызывая выход этой части;когда таймер отключается, вторая часть немедленно ставится в очередь.

Для реального кода, как указано в комментариях Panagiotis, рассмотрим Поток данных TPL для работы с очередями.

1 голос
/ 13 марта 2019

Это не прямой ответ на ваш вопрос.Я просто предлагаю альтернативу, которая намного приятнее, чем непосредственное использование задач.

Вы должны использовать Microsoft Reactive Framework (также известную как Rx) - NuGet System.Reactive и добавить using System.Reactive.Linq; - тогда вы можете сделать это:

public static async Task TaskSchedulerBehaviour()
{
    var topLevelTasks =
        from n in Observable.Range(0, 5)
        from batch in Observable.Range(0, 100000).Buffer(1000)
        from results in
            from step in batch.ToObservable()
            from result in Observable.FromAsync(() => WorkStep(n, step))
            select result
        select results;

    await topLevelTasks.ToArray();

    async Task WorkStep(int worker, int step)
    {
        if (step % 100 == 0)
        {
            Console.WriteLine($"worker={worker}, step={step}");
        }
        await Task.Delay(10);
    }
}

Rx очень хорошо обрабатывает все планирование для вас.

Вы должны признать, что код выглядит намного лучше.

...