Класс System.Threading.Tasks.TaskScheduler

В этой статье приводятся дополнительные замечания к справочной документации по этому API.

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

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

Класс TaskScheduler также служит точкой расширения для всей настраиваемой логики планирования. К ним относятся такие механизмы, как планирование задачи для выполнения, а также способ предоставления запланированных задач отладчикам. Если вам требуются специальные функциональные возможности, можно создать настраиваемый планировщик и включить его для определенных задач или запросов.

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

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

Глобальная очередь и локальные очереди

Пул потоков поддерживает глобальную рабочую очередь FIFO (в первую очередь) для потоков в каждом домене приложения. Всякий раз, когда программа вызывает ThreadPool.QueueUserWorkItem метод (или ThreadPool.UnsafeQueueUserWorkItem), работа помещается в эту общую очередь и в конечном итоге отменяет очередь в следующий поток, который становится доступным. Начиная с платформа .NET Framework 4, эта очередь использует алгоритм без блокировки, который напоминает ConcurrentQueue<T> класс. Используя эту реализацию без блокировки, пул потоков тратит меньше времени, когда он выполняет очереди и рабочие элементы без очередей. Это преимущество производительности доступно для всех программ, использующих пул потоков.

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

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

Кража работы

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

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

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

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

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

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

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

В следующем примере TaskScheduler.FromCurrentSynchronizationContext метод используется в приложении Windows Presentation Foundation (WPF) для планирования задачи в том же потоке, на который был создан элемент управления пользовательского интерфейса . В примере создается мозаика изображений, которые случайным образом выбираются из указанного каталога. Объекты WPF используются для загрузки и изменения размера изображений. Затем необработанные пиксели передаются в задачу, которая использует For цикл для записи данных пикселей в большой однобайтовый массив. Синхронизация не требуется, так как две плитки не занимают одни и те же элементы массива. Плитки также можно записать в любом порядке, так как их положение вычисляется независимо от любой другой плитки. Затем большой массив передается задаче, которая выполняется в потоке пользовательского интерфейса, где пиксельные данные загружаются в элемент управления Image.

Пример перемещает данные из потока пользовательского интерфейса, изменяет его с помощью параллельных циклов и Task объектов, а затем передает его обратно в задачу, которая выполняется в потоке пользовательского интерфейса. Этот подход полезен при использовании параллельной библиотеки задач для выполнения операций, которые либо не поддерживаются API WPF, либо недостаточно быстро. Еще одним способом создания мозаики изображений в WPF является использование System.Windows.Controls.WrapPanel элемента управления и добавление изображений в него. Обрабатывает WrapPanel работу размещения плиток. Однако эта работа может выполняться только в потоке пользовательского интерфейса.

using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace WPF_CS1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private int fileCount;
        int colCount;
        int rowCount;
        private int tilePixelHeight;
        private int tilePixelWidth;
        private int largeImagePixelHeight;
        private int largeImagePixelWidth;
        private int largeImageStride;
        PixelFormat format;
        BitmapPalette palette = null;

        public MainWindow()
        {
            InitializeComponent();

            // For this example, values are hard-coded to a mosaic of 8x8 tiles.
            // Each tile is 50 pixels high and 66 pixels wide and 32 bits per pixel.
            colCount = 12;
            rowCount = 8;
            tilePixelHeight = 50;
            tilePixelWidth = 66;
            largeImagePixelHeight = tilePixelHeight * rowCount;
            largeImagePixelWidth = tilePixelWidth * colCount;
            largeImageStride = largeImagePixelWidth * (32 / 8);
            this.Width = largeImagePixelWidth + 40;
            image.Width = largeImagePixelWidth;
            image.Height = largeImagePixelHeight;
        }

        private void button_Click(object sender, RoutedEventArgs e)
        {

            // For best results use 1024 x 768 jpg files at 32bpp.
            string[] files = System.IO.Directory.GetFiles(@"C:\Users\Public\Pictures\Sample Pictures\", "*.jpg");

            fileCount = files.Length;
            Task<byte[]>[] images = new Task<byte[]>[fileCount];
            for (int i = 0; i < fileCount; i++)
            {
                int x = i;
                images[x] = Task.Factory.StartNew(() => LoadImage(files[x]));
            }

            // When they've all been loaded, tile them into a single byte array.
            var tiledImage = Task.Factory.ContinueWhenAll(
                images, (i) => TileImages(i));

            // We are currently on the UI thread. Save the sync context and pass it to
            // the next task so that it can access the UI control "image".
            var UISyncContext = TaskScheduler.FromCurrentSynchronizationContext();

            // On the UI thread, put the bytes into a bitmap and
            // display it in the Image control.
            var t3 = tiledImage.ContinueWith((antecedent) =>
            {
                // Get System DPI.
                Matrix m = PresentationSource.FromVisual(Application.Current.MainWindow)
                                            .CompositionTarget.TransformToDevice;
                double dpiX = m.M11;
                double dpiY = m.M22;

                BitmapSource bms = BitmapSource.Create(largeImagePixelWidth,
                    largeImagePixelHeight,
                    dpiX,
                    dpiY,
                    format,
                    palette, //use default palette
                    antecedent.Result,
                    largeImageStride);
                image.Source = bms;
            }, UISyncContext);
        }

        byte[] LoadImage(string filename)
        {
            // Use the WPF BitmapImage class to load and
            // resize the bitmap. NOTE: Only 32bpp formats are supported correctly.
            // Support for additional color formats is left as an exercise
            // for the reader. For more information, see documentation for ColorConvertedBitmap.

            BitmapImage bitmapImage = new BitmapImage();
            bitmapImage.BeginInit();
            bitmapImage.UriSource = new Uri(filename);
            bitmapImage.DecodePixelHeight = tilePixelHeight;
            bitmapImage.DecodePixelWidth = tilePixelWidth;
            bitmapImage.EndInit();

            format = bitmapImage.Format;
            int size = (int)(bitmapImage.Height * bitmapImage.Width);
            int stride = (int)bitmapImage.Width * 4;
            byte[] dest = new byte[stride * tilePixelHeight];

            bitmapImage.CopyPixels(dest, stride, 0);

            return dest;
        }

        int Stride(int pixelWidth, int bitsPerPixel)
        {
            return (((pixelWidth * bitsPerPixel + 31) / 32) * 4);
        }

        // Map the individual image tiles to the large image
        // in parallel. Any kind of raw image manipulation can be
        // done here because we are not attempting to access any
        // WPF controls from multiple threads.
        byte[] TileImages(Task<byte[]>[] sourceImages)
        {
            byte[] largeImage = new byte[largeImagePixelHeight * largeImageStride];
            int tileImageStride = tilePixelWidth * 4; // hard coded to 32bpp

            Random rand = new Random();
            Parallel.For(0, rowCount * colCount, (i) =>
            {
                // Pick one of the images at random for this tile.
                int cur = rand.Next(0, sourceImages.Length);
                byte[] pixels = sourceImages[cur].Result;

                // Get the starting index for this tile.
                int row = i / colCount;
                int col = (int)(i % colCount);
                int idx = ((row * (largeImageStride * tilePixelHeight)) + (col * tileImageStride));

                // Write the pixels for the current tile. The pixels are not contiguous
                // in the array, therefore we have to advance the index by the image stride
                // (minus the stride of the tile) for each scanline of the tile.
                int tileImageIndex = 0;
                for (int j = 0; j < tilePixelHeight; j++)
                {
                    // Write the next scanline for this tile.
                    for (int k = 0; k < tileImageStride; k++)
                    {
                        largeImage[idx++] = pixels[tileImageIndex++];
                    }
                    // Advance to the beginning of the next scanline.
                    idx += largeImageStride - tileImageStride;
                }
            });
            return largeImage;
        }
    }
}

Partial Public Class MainWindow : Inherits Window
    Dim fileCount As Integer
    Dim colCount As Integer
    Dim rowCount As Integer
    Dim tilePixelHeight As Integer
    Dim tilePixelWidth As Integer
    Dim largeImagePixelHeight As Integer
    Dim largeImagePixelWidth As Integer
    Dim largeImageStride As Integer
    Dim format As PixelFormat
    Dim palette As BitmapPalette = Nothing

    Public Sub New()
        InitializeComponent()

        ' For this example, values are hard-coded to a mosaic of 8x8 tiles.
        ' Each tile Is 50 pixels high and 66 pixels wide and 32 bits per pixel.
        colCount = 12
        rowCount = 8
        tilePixelHeight = 50
        tilePixelWidth = 66
        largeImagePixelHeight = tilePixelHeight * rowCount
        largeImagePixelWidth = tilePixelWidth * colCount
        largeImageStride = largeImagePixelWidth * (32 / 8)
        Me.Width = largeImagePixelWidth + 40
        image.Width = largeImagePixelWidth
        image.Height = largeImagePixelHeight
    End Sub

    Private Sub button_Click(sender As Object, e As RoutedEventArgs) _
        Handles button.Click

        ' For best results use 1024 x 768 jpg files at 32bpp.
        Dim files() As String = System.IO.Directory.GetFiles("C:\Users\Public\Pictures\Sample Pictures\", "*.jpg")

        fileCount = files.Length
        Dim images(fileCount - 1) As Task(Of Byte())
        For i As Integer = 0 To fileCount - 1
            Dim x As Integer = i
            images(x) = Task.Factory.StartNew(Function() LoadImage(files(x)))
        Next

        ' When they have all been loaded, tile them into a single byte array.
        'var tiledImage = Task.Factory.ContinueWhenAll(
        '        images, (i) >= TileImages(i));

        '        Dim tiledImage As Task(Of Byte()) = Task.Factory.ContinueWhenAll(images, Function(i As Task(Of Byte())) TileImages(i))
        Dim tiledImage = Task.Factory.ContinueWhenAll(images, Function(i As Task(Of Byte())()) TileImages(i))
        ' We are currently on the UI thread. Save the sync context and pass it to
        ' the next task so that it can access the UI control "image1".
        Dim UISyncContext = TaskScheduler.FromCurrentSynchronizationContext()

        ' On the UI thread, put the bytes into a bitmap and
        ' display it in the Image control.
        Dim t3 = tiledImage.ContinueWith(Sub(antecedent)
                                             ' Get System DPI.
                                             Dim m As Matrix = PresentationSource.FromVisual(Application.Current.MainWindow).CompositionTarget.TransformToDevice
                                             Dim dpiX As Double = m.M11
                                             Dim dpiY As Double = m.M22

                                             ' Use the default palette in creating the bitmap.
                                             Dim bms As BitmapSource = BitmapSource.Create(largeImagePixelWidth,
                                                                                           largeImagePixelHeight,
                                             dpiX,
                                             dpiY,
                                             format,
                                             palette,
                                             antecedent.Result,
                                             largeImageStride)
                                             image.Source = bms
                                         End Sub, UISyncContext)
    End Sub

    Public Function LoadImage(filename As String) As Byte()
        ' Use the WPF BitmapImage class to load and 
        ' resize the bitmap. NOTE: Only 32bpp formats are supported correctly.
        ' Support for additional color formats Is left as an exercise
        ' for the reader. For more information, see documentation for ColorConvertedBitmap.
        Dim bitmapImage As New BitmapImage()
        bitmapImage.BeginInit()
        bitmapImage.UriSource = New Uri(filename)
        bitmapImage.DecodePixelHeight = tilePixelHeight
        bitmapImage.DecodePixelWidth = tilePixelWidth
        bitmapImage.EndInit()

        format = bitmapImage.Format
        Dim size As Integer = CInt(bitmapImage.Height * bitmapImage.Width)
        Dim stride As Integer = CInt(bitmapImage.Width * 4)
        Dim dest(stride * tilePixelHeight - 1) As Byte

        bitmapImage.CopyPixels(dest, stride, 0)

        Return dest
    End Function

    Function Stride(pixelWidth As Integer, bitsPerPixel As Integer) As Integer
        Return (((pixelWidth * bitsPerPixel + 31) / 32) * 4)
    End Function

    ' Map the individual image tiles to the large image
    ' in parallel. Any kind of raw image manipulation can be
    ' done here because we are Not attempting to access any 
    ' WPF controls from multiple threads.
    Function TileImages(sourceImages As Task(Of Byte())()) As Byte()
        Dim largeImage(largeImagePixelHeight * largeImageStride - 1) As Byte
        Dim tileImageStride As Integer = tilePixelWidth * 4 ' hard coded To 32bpp

        Dim rand As New Random()
        Parallel.For(0, rowCount * colCount, Sub(i)
                                                 ' Pick one of the images at random for this tile.
                                                 Dim cur As Integer = rand.Next(0, sourceImages.Length)
                                                 Dim pixels() As Byte = sourceImages(cur).Result

                                                 ' Get the starting index for this tile.
                                                 Dim row As Integer = i \ colCount
                                                 Dim col As Integer = i Mod colCount
                                                 Dim idx As Integer = ((row * (largeImageStride * tilePixelHeight)) + (col * tileImageStride))

                                                 ' Write the pixels for the current tile. The pixels are Not contiguous
                                                 ' in the array, therefore we have to advance the index by the image stride
                                                 ' (minus the stride of the tile) for each scanline of the tile.
                                                 Dim tileImageIndex As Integer = 0
                                                 For j As Integer = 0 To tilePixelHeight - 1
                                                     ' Write the next scanline for this tile.
                                                     For k As Integer = 0 To tileImageStride - 1
                                                         largeImage(idx) = pixels(tileImageIndex)
                                                         idx += 1
                                                         tileImageIndex += 1
                                                     Next
                                                     ' Advance to the beginning of the next scanline.
                                                     idx += largeImageStride - tileImageStride
                                                 Next
                                             End Sub)
        Return largeImage
    End Function
End Class

Чтобы создать пример, создайте проект приложения WPF в Visual Studio и назовите его WPF_CS1 (для проекта WPF C#) или WPF_VB1 (для проекта WPF Visual Basic). После этого выполните описанные ниже действия.

  1. В режиме конструктора перетащите Image элемент управления из панели элементов в левый верхний угол области конструктора. В текстовом поле "Имя" окна "Свойства" присвойте элементу управления "изображение".

  2. Перетащите Button элемент управления из панели элементов в левую часть окна приложения. В представлении XAML укажите свойство кнопки как "Создать мозаику" и укажите Content его Width свойство как "100". Click Подключение событие с обработчиком button_Click событий, определенным в коде примера, путем добавления Click="button_Click" в <Button> элемент. В текстовом поле "Имя" окна "Свойства" назовите элемент управления "Button".

  3. Замените все содержимое файла MainWindow.xaml.cs или MainWindow.xaml.vb кодом из этого примера. Для проекта WPF C# убедитесь, что имя рабочей области соответствует имени проекта.

  4. В примере считываются изображения JPEG из каталога C:\Users\Public\Pictures\Sample Pictures. Создайте каталог и поместите в него некоторые изображения или измените путь для ссылки на другой каталог, содержащий изображения.

В этом примере есть некоторые ограничения. Например, поддерживаются только 32-разрядные изображения на пиксель; изображения в других форматах повреждены BitmapImage объектом во время операции изменения размера. Кроме того, исходные изображения должны быть больше размера плитки. В качестве дальнейшего упражнения можно добавить функциональные возможности для обработки нескольких форматов пикселей и размеров файлов.