Classe System.Threading.Tasks.TaskScheduler

Cet article vous offre des remarques complémentaires à la documentation de référence pour cette API.

La TaskScheduler classe représente un planificateur de tâches. Un planificateur de tâches s'assure que le travail d'une tâche est finalement exécuté.

Le planificateur de tâches par défaut fournit un vol de travail pour l’équilibrage de charge, l’injection de threads/la mise hors service pour un débit maximal et de bonnes performances globales. Ce doit être suffisant pour la plupart des scénarios.

La TaskScheduler classe sert également de point d’extension pour toute logique de planification personnalisable. Cela inclut des mécanismes tels que la planification d’une tâche pour l’exécution et la façon dont les tâches planifiées doivent être exposées aux débogueurs. Si vous avez besoin de fonctionnalités spéciales, vous pouvez créer un planificateur personnalisé et l’activer pour des tâches ou des requêtes spécifiques.

Planificateur de tâches par défaut et pool de threads

Le planificateur par défaut pour la bibliothèque parallèle de tâches et PLINQ utilise le pool de threads .NET, représenté par la classe, pour mettre en file d’attente et exécuter le ThreadPool travail. Le pool de threads utilise les informations fournies par le Task type pour prendre en charge efficacement le parallélisme affiné (unités de travail de courte durée) que représentent souvent les tâches et requêtes parallèles.

File d’attente globale et files d’attente locales

Le pool de threads gère une file d’attente de travail FIFO globale (premier entré, premier sortie) pour les threads de chaque domaine d’application. Chaque fois qu’un programme appelle la ThreadPool.QueueUserWorkItem méthode (ou ThreadPool.UnsafeQueueUserWorkItem), le travail est placé sur cette file d’attente partagée et finalement dé-mis en file d’attente sur le thread suivant qui devient disponible. À compter de .NET Framework 4, cette file d’attente utilise un algorithme sans verrou qui ressemble à la ConcurrentQueue<T> classe. En utilisant cette implémentation sans verrou, le pool de threads passe moins de temps quand il met en file d’attente et dé-file d’attente les éléments de travail. Cet avantage de performances est disponible pour tous les programmes qui utilisent le pool de threads.

Les tâches de niveau supérieur, qui ne sont pas créées dans le contexte d’une autre tâche, sont mises en file d’attente globale comme tout autre élément de travail. Toutefois, les tâches imbriquées ou enfants, créées dans le contexte d’une autre tâche, sont gérées tout à fait différemment. Une tâche enfant ou imbriquée est placée dans une file d'attente locale qui est spécifique au thread sur lequel s'exécute la tâche parente. La tâche parente peut être une tâche de niveau supérieur ou l’enfant d’une autre tâche. Quand ce thread est prêt pour exécuter davantage de travail, il regarde en premier dans la file d'attente locale. Les éléments de travail qui s’y trouvent éventuellement peuvent être récupérés rapidement. Les files d’attente locales sont accessibles dans le dernier ordre de première sortie (LIFO) pour préserver la localité du cache et réduire la contention. Pour plus d’informations sur les tâches enfants et les tâches imbriquées, consultez Tâches enfants jointes et détachées.

L’utilisation de files d’attente locales réduit non seulement la pression sur la file d’attente globale, mais tire également parti de la localité des données. Les éléments de travail de la file d’attente locale font souvent référence à des structures de données physiquement proches les unes des autres en mémoire. Dans ce cas, les données se trouvent déjà dans le cache après l’exécution de la première tâche et sont accessibles rapidement. Parallel LINQ (PLINQ) et la Parallel classe utilisent largement les tâches imbriquées et les tâches enfants, et effectuent des accélérations significatives à l’aide des files d’attente de travail locales.

Vol de travail

À compter de .NET Framework 4, le pool de threads propose également un algorithme de vol de travail pour vous assurer qu’aucun thread n’est inactif alors que d’autres travaillent toujours dans leurs files d’attente. Quand un thread de pool de threads est prêt à effectuer davantage de travail, il regarde successivement en tête de sa file d'attente locale, dans la file d'attente globale, puis dans les files d'attente locales d'autres threads. S’il trouve un élément de travail dans la file d’attente locale d’un autre thread, il applique d’abord des méthodes heuristiques pour s’assurer qu’il peut exécuter le travail efficacement. S’il le peut, il supprime l’élément de travail de la queue (dans l’ordre FIFO). Cela réduit les conflits sur chaque file d'attente locale et permet de conserver la localité des données. Cette architecture aide le travail d’équilibreur de charge du pool de threads plus efficacement que les versions précédentes.

Tâches longues

Vous pouvez explicitement empêcher une tâche d'être mise en file d'attente locale. Par exemple, vous savez peut-être qu’un élément de travail particulier fonctionnera pour une durée relativement longue et sera à même de bloquer tous les autres éléments de travail sur la file d’attente locale. Dans ce cas, vous pouvez spécifier l’option System.Threading.Tasks.TaskCreationOptions, qui indique au planificateur qu’un thread supplémentaire peut être nécessaire pour la tâche afin qu’elle n’entrave pas la progression d’autres threads ou éléments de travail sur la file d’attente locale. En utilisant cette option, vous évitez complètement le pool de threads, y compris les files d’attente globales et locales.

Inlining des tâches

Dans certains cas, lorsqu’une Task attente est activée, elle peut être exécutée de manière synchrone sur le thread qui effectue l’opération d’attente. Cela améliore les performances en empêchant la nécessité d’un thread supplémentaire et en utilisant plutôt le thread existant, ce qui aurait été bloqué dans le cas contraire. Pour éviter les erreurs en raison d’une réentrance, l’incorporation de tâches se produit uniquement lorsque la cible d’attente se trouve dans la file d’attente locale du thread approprié.

Spécifier un contexte de synchronisation

Vous pouvez utiliser la méthode TaskScheduler.FromCurrentSynchronizationContext pour spécifier qu’une tâche doit être planifiée pour fonctionner sur un thread particulier. Cela s'avère utile dans les infrastructures telles que Windows Forms et Windows Presentation Foundation où l'accès aux objets d'interface utilisateur est souvent restreint au code qui s'exécute sur le thread sur lequel l'objet d'interface utilisateur a été créé.

L’exemple suivant utilise la TaskScheduler.FromCurrentSynchronizationContext méthode dans une application Windows Presentation Foundation (WPF) pour planifier une tâche sur le même thread que celui sur lequel le contrôle d’interface utilisateur a été créé. L’exemple crée une mosaïque d’images sélectionnées de manière aléatoire à partir d’un répertoire spécifié. Les objets WPF sont utilisés pour charger et redimensionner les images. Les pixels bruts sont ensuite passés à une tâche qui utilise une For boucle pour écrire les données de pixels dans un grand tableau d’octets. Aucune synchronisation n’est requise, car aucune deux vignettes n’occupent les mêmes éléments de tableau. Les vignettes peuvent également être écrites dans n’importe quel ordre, car leur position est calculée indépendamment de toute autre vignette. Le grand tableau est ensuite passé à une tâche qui s’exécute sur le thread d’interface utilisateur, où les données de pixels sont chargées dans un contrôle Image.

L’exemple déplace les données hors du thread d’interface utilisateur, les modifie à l’aide de boucles et Task d’objets parallèles, puis les transmet à une tâche qui s’exécute sur le thread d’interface utilisateur. Cette approche est utile lorsque vous devez utiliser la bibliothèque parallèle de tâches pour effectuer des opérations qui ne sont pas prises en charge par l’API WPF ou qui ne sont pas suffisamment rapides. Une autre façon de créer une mosaïque d’images dans WPF consiste à utiliser un System.Windows.Controls.WrapPanel contrôle et à y ajouter des images. Gère WrapPanel le travail de positionnement des vignettes. Toutefois, ce travail ne peut être effectué que sur le thread d’interface utilisateur.

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

Pour créer l’exemple, créez un projet d’application WPF dans Visual Studio et nommez-le WPF_CS1 (pour un projet WPF C#) ou WPF_VB1 (pour un projet WPF Visual Basic). Faites ensuite ce qui suit :

  1. En mode Création, faites glisser un Image contrôle de la boîte à outils dans le coin supérieur gauche de l’aire de conception. Dans la zone de texte Name de la fenêtre Propriétés , nommez le contrôle « image ».

  2. Faites glisser un Button contrôle de la boîte à outils vers la partie inférieure gauche de la fenêtre d’application. En mode XAML, spécifiez la Content propriété du bouton comme « Créer une mosaïque » et spécifiez sa Width propriété comme « 100 ». Connecter l’événement Click avec le button_Click gestionnaire d’événements défini dans le code de l’exemple en ajoutant Click="button_Click" à l’élément<Button>. Dans la zone de texte Nom de la fenêtre Propriétés , nommez le contrôle « button ».

  3. Remplacez tout le contenu du fichier MainWindow.xaml.cs ou MainWindow.xaml.vb par le code de cet exemple. Pour un projet WPF C#, vérifiez que le nom de l’espace de travail correspond au nom du projet.

  4. L’exemple lit des images JPEG à partir d’un répertoire nommé C :\Users\Public\Pictures\Sample Pictures. Créez le répertoire et placez-y certaines images, ou modifiez le chemin d’accès pour faire référence à un autre répertoire qui contient des images.

Cet exemple présente certaines limitations. Par exemple, seules les images de 32 bits par pixel sont prises en charge ; les images dans d’autres formats sont endommagées par l’objet BitmapImage pendant l’opération de redimensionnement. En outre, les images sources doivent toutes être supérieures à la taille de la vignette. En guise d’exercice supplémentaire, vous pouvez ajouter des fonctionnalités pour gérer plusieurs formats de pixels et tailles de fichier.