Поделиться через


Планировщики заданий

Планировщики заданий представлены классом System.Threading.Tasks.TaskScheduler. Планировщик заданий гарантирует завершение работы над задачей. Планировщик заданий по умолчанию основан на платформе .NET Framework 4 ThreadPool, которая обеспечивает перенос нагрузки для обеспечения ее балансировки, вставки/удаления потока для обеспечения максимальной пропускной способности и общего повышения производительности. Может применяться для большинства сценариев. Однако если требуется специальные функциональные возможности, можно создать пользовательский планировщик и включить его для определенных задач и очередей. Дополнительные сведения о создании и использовании пользовательского планировщика заданий см. Практическое руководство. Создание планировщика заданий, ограничивающего степень параллелизма. Дополнительные примеры пользовательских планировщиков см. в разделе Parallel Extensions Samples на веб-сайте коллекции кодов MSDN.

Планировщик заданий по умолчанию и ThreadPool

Планировщик по умолчанию для библиотеки параллельных задач и PLINQ использует .NET Framework ThreadPool для создания очереди и выполнения работы. В .NET Framework 4, ThreadPool использует сведения, предоставляемые типом System.Threading.Tasks.Task для эффективной поддержки точного параллелизма (элементов работы с небольшим временем существования), которые зачастую представляют параллельные задачи и запросы.

Сравнение глобальной очереди ThreadPool слокальными очередями

Как и в более ранних версиях .NET Framework, ThreadPool поддерживает глобальную очередь обработки FIFO (первым пришел — первым обслужен) для потоков в каждом домене приложения. При вызове программой QueueUserWorkItem (или UnsafeQueueUserWorkItem), процесс помещается в такую общую очередь и в конце концов переходит в свободный поток. В .NET Framework 4 эта очередь улучшена для использования алгоритма без блокировки, который похож на класс ConcurrentQueue. Благодаря отсутствию блокировки объекту ThreadPool требуется меньше времени при построении очереди рабочих элементов и их извлечении из очереди. Такое преимущество в производительности доступно для всех программ, использующих ThreadPool.

Задачи первого уровня, которые не являются задачами, созданными в контексте других задач, помещаются в глобальную очередь, также как и любой другой рабочий элемент. Однако вложенные или дочерние задачи, создаваемые в контексте других задач, обрабатываются по-другому. Дочерние или вложенные задачи помещаются в локальную очередь, относящуюся к потоку, в котором выполняется родительская задача. Родительская задача может быть задачей первого уровня или дочерней по отношению к другой задаче. Когда такой поток готов для работы, в первую очередь он обращается к локальной очереди. К ожидающим в ней рабочим элементам можно получить быстрый доступ. Доступ к локальным очередям осуществляется в порядке "последним поступил — первым обслужен" (LIFO) для сохранения расположения кэша и снижения вероятности возникновения конфликта. Дополнительные сведения о дочерних и вложенных задачах см. в разделе Вложенные и дочерние задачи.

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

Sub QueueTasks()

    ' TaskA is a top level task.
    Dim taskA = Task.Factory.StartNew(Sub()

                                          Console.WriteLine("I was enqueued on the thread pool's global queue.")

                                          ' TaskB is a nested task and TaskC is a child task. Both go to local queue.
                                          Dim taskB = New Task(Sub() Console.WriteLine("I was enqueued on the local queue."))
                                          Dim taskC = New Task(Sub() Console.WriteLine("I was enqueued on the local queue, too."),
                                                                  TaskCreationOptions.AttachedToParent)

                                          taskB.Start()
                                          taskC.Start()

                                      End Sub)
End Sub
void QueueTasks()
{
    // TaskA is a top level task.
    Task taskA = Task.Factory.StartNew( () =>
    {                
        Console.WriteLine("I was enqueued on the thread pool's global queue."); 

        // TaskB is a nested task and TaskC is a child task. Both go to local queue.
        Task taskB = new Task( ()=> Console.WriteLine("I was enqueued on the local queue."));
        Task taskC = new Task(() => Console.WriteLine("I was enqueued on the local queue, too."),
                                TaskCreationOptions.AttachedToParent);

        taskB.Start();
        taskC.Start();

    });
}

Использование локальных очередей не только снижает нагрузку на глобальную очередь, но и использует преимущества локальности данных. Рабочие элементы в локальной очереди часто ссылаются на структуры данных, которые находятся в памяти рядом друг с другом. В этом случае данные уже находятся в кэше после выполнения первой задачи, и к ним можно получить быстрый доступ. Как Parallel LINQ (PLINQ), так и Parallel классы широко используют вложенные и дочерние задачи, и получают значительное увеличение скорости посредством использования локальных рабочих очередей.

Перенос нагрузки

.NET Framework 4 ThreadPool также имеет алгоритм переноса нагрузки для обеспечения загрузки всех потоков, в то время как другие работают над своими очередями. Когда поток из пула потоков готов для обработки большего количества работы, сначала он обращается к заголовку локальной, затем глобальной очереди, а затем к локальным очередям других потоков. При обнаружении рабочего элемента в локальной очереди другого потока, он сначала применяет эвристику, для подтверждения эффективности выполняемой работы. По возможности, он выводит из очереди рабочий элемент, находящийся в конце (в порядке FIFO). Это позволяет снизить вероятность возникновения конфликта в каждой локальной очереди и сохранить локальность данных. Такая архитектура помогает объекту .NET Framework 4 ThreadPool более эффективно распределять рабочую нагрузку по сравнению с предыдущими версиями.

Длительные задачи

Может возникнуть необходимость в предотвращении отнесения задачи в локальную очередь. Например, может быть известно, что определенный рабочий элемент будет выполняться на протяжении длительного периода времени и может заблокировать другие рабочие элементы в локальной очереди. В этом случае можно указать параметр LongRunning, который подсказывает планировщику, что для выполнения задачи может потребоваться дополнительный поток, чтобы она не заблокировала дальнейший ход работы в других потоках или рабочих элементах в локальной очереди. С помощью этого параметра можно полностью избежать ThreadPool, включая глобальные и локальные очереди.

Встраивание задачи

В некоторых случаях, когда Задача ожидает, она может быть выполнена синхронно в Потоке, выполняющем операцию ожидания. Это позволяет увеличить производительность, так как отсутствует необходимость в дополнительном Потоке за счет использования существующего, который будет заблокирован. Чтобы избежать ошибок из-за повторного входа, встраивание задачи происходит только при обнаружении цели ожидания в соответственной локальной очереди Потока.

Указание контекста синхронизации

Чтобы указать, что задача должна выполняться в определенном потоке, можно воспользоваться методом TaskScheduler.FromCurrentSynchronizationContext. Это полезно в таких платформах, как Windows Forms и Windows Presentation Foundation, где доступ к объектам пользовательского интерфейса зачастую имеет только код, выполняемый в том же потоке, в котором создан объект пользовательского интерфейса. Дополнительные сведения см. в разделе Практическое руководство. Планирование работы в указанном контексте синхронизации.

См. также

Ссылки

TaskScheduler

Основные понятия

Библиотека параллельных задач

Практическое руководство. Планирование работы в указанном контексте синхронизации

Другие ресурсы

Практическое руководство. Создание планировщика заданий, ограничивающего степень параллелизма

Фабрики задач