Classe System.Threading.Tasks.TaskScheduler

Questo articolo fornisce osservazioni supplementari alla documentazione di riferimento per questa API.

La TaskScheduler classe rappresenta un'utilità di pianificazione dell'attività. Un'utilità di pianificazione assicura che il lavoro di un'attività viene eseguito.

L'utilità di pianificazione predefinita offre il furto di lavoro per il bilanciamento del carico, l'inserimento/ritiro del thread per la velocità effettiva massima e prestazioni complessive ottimali. Dovrebbe essere sufficiente per la maggior parte degli scenari.

La TaskScheduler classe funge anche da punto di estensione per tutta la logica di pianificazione personalizzabile. Sono inclusi meccanismi come la pianificazione di un'attività per l'esecuzione e il modo in cui le attività pianificate devono essere esposte ai debugger. Se sono necessarie funzionalità speciali, è possibile creare un'utilità di pianificazione personalizzata e abilitarla per attività o query specifiche.

Utilità di pianificazione predefinita e pool di thread

L'utilità di pianificazione predefinita per Task Parallel Library e PLINQ usa il pool di thread .NET, rappresentato dalla ThreadPool classe , per accodare ed eseguire il lavoro. Il pool di thread usa le informazioni fornite dal Task tipo per supportare in modo efficiente il parallelismo con granularità fine (unità di lavoro di breve durata) che spesso rappresentano attività e query parallele.

Coda globale e code locali

Il pool di thread gestisce una coda di lavoro FIFO globale (first-in, first-out) per i thread in ogni dominio applicazione. Ogni volta che un programma chiama il ThreadPool.QueueUserWorkItem metodo (o ThreadPool.UnsafeQueueUserWorkItem), il lavoro viene inserito in questa coda condivisa e infine de-accodato nel thread successivo che diventa disponibile. A partire da .NET Framework 4, questa coda usa un algoritmo senza blocco simile alla ConcurrentQueue<T> classe . Usando questa implementazione senza blocco, il pool di thread impiega meno tempo quando accoda e de-accoda gli elementi di lavoro. Questo vantaggio per le prestazioni è disponibile per tutti i programmi che usano il pool di thread.

Le attività di primo livello, ovvero quelle non create nell'ambito di un'altra attività, vengono inserite nella coda globale come qualsiasi altro elemento di lavoro. Tuttavia le attività annidate o figlio, create nell'ambito di un'altra attività, vengono gestite in modo molto diverso. Un'attività figlio o annidata viene inserita in una coda locale specifica del thread in cui è in esecuzione l'attività padre. L'attività padre può essere un'attività di primo livello o anche l'elemento figlio di un'altra attività. Quando questo thread è disponibile per altro lavoro, esegue innanzitutto una ricerca nella coda locale. Se sono presenti elementi di lavoro in attesa, è possibile accedervi rapidamente. È possibile accedere alle code locali nell'ultimo ordine di first-out (LIFO) per mantenere la località della cache e ridurre i conflitti. Per altre informazioni sulle attività figlio e sulle attività annidate, vedere Attività figlio collegate e scollegate.

L'uso di code locali non solo riduce la pressione sulla coda globale, ma sfrutta anche la località dei dati. Gli elementi di lavoro nella coda locale fanno spesso riferimento a strutture di dati fisicamente vicine tra loro in memoria. In questi casi, i dati si trovano già nella cache dopo l'esecuzione della prima attività e possono essere accessibili rapidamente. Sia PARALLEL LINQ (PLINQ) che la Parallel classe usano attività annidate e attività figlio in modo esteso e ottengono velocità significative usando le code di lavoro locali.

Furto di lavoro

A partire da .NET Framework 4, il pool di thread include anche un algoritmo di furto di lavoro per assicurarsi che nessun thread sia inattiva mentre altri hanno ancora lavoro nelle code. Quando un thread del pool di thread è disponibile per altro lavoro, effettua prima una ricerca all'inizio della propria coda locale, quindi nella coda globale e infine nelle code locali degli altri thread. Se trova un elemento di lavoro nella coda locale di un altro thread, prima applica l'euristica per avere la certezza di poter eseguire il lavoro in modo efficiente. Se possibile, de-accoda l'elemento di lavoro dalla parte finale (in ordine FIFO). In questo modo viene ridotto il conflitto in ogni coda locale e viene mantenuta la località dei dati. Questa architettura consente di bilanciare il carico del pool di thread in modo più efficiente rispetto alle versioni precedenti.

Attività a esecuzione prolungata

Può essere opportuno impedire in modo esplicito l'inserimento di un'attività in una coda locale. Ad esempio, si potrebbe sapere che un particolare elemento di lavoro verrà eseguito per un tempo relativamente lungo e probabilmente bloccherà tutti gli altri elementi di lavoro nella coda locale. In questo caso, è possibile specificare l'opzione System.Threading.Tasks.TaskCreationOptions, che fornisce all'utilità di pianificazione il suggerimento che per l'attività potrebbe essere necessario un altro thread in modo che non blocchi l'avanzamento di altri thread o elementi di lavoro nella coda locale. Usando questa opzione si evita completamente il pool di thread, incluse le code globali e locali.

Inlining delle attività

In alcuni casi quando un oggetto Task è in attesa, può essere eseguito in modo sincrono sul thread che esegue l'operazione di attesa. Ciò migliora le prestazioni impedendo la necessità di un thread aggiuntivo e usando invece il thread esistente, che sarebbe stato bloccato in caso contrario. Per evitare errori dovuti alla reentrancy, l'inlining delle attività si verifica solo quando la destinazione di attesa viene trovata nella coda locale del thread pertinente.

Specificare un contesto di sincronizzazione

È possibile usare il metodo TaskScheduler.FromCurrentSynchronizationContext per specificare che è necessario pianificare l'esecuzione di un'attività in un particolare thread. Ciò risulta utile in framework come Windows Form e Windows Presentation Foundation dove l'accesso agli oggetti dell'interfaccia utente è spesso limitato alla coda in esecuzione nello stesso thread in cui è stato creato l'oggetto dell'interfaccia utente.

L'esempio seguente usa il TaskScheduler.FromCurrentSynchronizationContext metodo in un'app Windows Presentation Foundation (WPF) per pianificare un'attività nello stesso thread in cui è stato creato il controllo dell'interfaccia utente. Nell'esempio viene creato un mosaico di immagini selezionate in modo casuale da una directory specificata. Gli oggetti WPF vengono usati per caricare e ridimensionare le immagini. I pixel non elaborati vengono quindi passati a un'attività che usa un For ciclo per scrivere i dati pixel in una matrice a byte singolo di grandi dimensioni. Non è necessaria alcuna sincronizzazione perché non sono presenti due riquadri che occupano gli stessi elementi della matrice. I riquadri possono anche essere scritti in qualsiasi ordine perché la loro posizione viene calcolata indipendentemente da qualsiasi altro riquadro. La matrice di grandi dimensioni viene quindi passata a un'attività eseguita nel thread dell'interfaccia utente, in cui i dati pixel vengono caricati in un controllo Immagine.

L'esempio sposta i dati dal thread dell'interfaccia utente, lo modifica usando cicli e Task oggetti paralleli e quindi lo passa a un'attività eseguita nel thread dell'interfaccia utente. Questo approccio è utile quando è necessario usare Task Parallel Library per eseguire operazioni non supportate dall'API WPF o non sono sufficientemente veloci. Un altro modo per creare un mosaico di immagini in WPF consiste nell'usare un System.Windows.Controls.WrapPanel controllo e aggiungervi immagini. Gestisce WrapPanel il lavoro di posizionamento dei riquadri. Tuttavia, questo lavoro può essere eseguito solo nel thread dell'interfaccia utente.

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

Per creare l'esempio, creare un progetto di applicazione WPF in Visual Studio e denominarlo WPF_CS1 (per un progetto WPF C#) o WPF_VB1 (per un progetto WPF di Visual Basic). Eseguire quindi le operazioni seguenti:

  1. Nella visualizzazione progettazione trascinare un Image controllo dalla Casella degli strumenti nell'angolo superiore sinistro dell'area di progettazione. Nella casella di testo Nome della finestra Proprietà assegnare al controllo il nome "image".

  2. Trascinare un Button controllo dalla casella degli strumenti nella parte inferiore sinistra della finestra dell'applicazione. Nella visualizzazione XAML specificare la Content proprietà del pulsante come "Make a mosaic" e specificare la relativa Width proprietà come "100". Connessione l'evento Click con il button_Click gestore eventi definito nel codice dell'esempio aggiungendo Click="button_Click" all'elemento <Button> . Nella casella di testo Nome della finestra Proprietà assegnare al controllo il nome "button".

  3. Sostituire l'intero contenuto del file MainWindow.xaml.cs o MainWindow.xaml.vb con il codice di questo esempio. Per un progetto WPF C#, assicurarsi che il nome dell'area di lavoro corrisponda al nome del progetto.

  4. L'esempio legge immagini JPEG da una directory denominata C:\Users\Public\Pictures\Sample Pictures. Creare la directory e inserirle alcune immagini oppure modificare il percorso per fare riferimento ad altre directory che contengono immagini.

Questo esempio presenta alcune limitazioni. Ad esempio, sono supportate solo immagini a 32 bit per pixel; Le immagini in altri formati sono danneggiate dall'oggetto durante l'operazione BitmapImage di ridimensionamento. Inoltre, le immagini di origine devono essere tutte maggiori delle dimensioni del riquadro. Come ulteriore esercizio, è possibile aggiungere funzionalità per gestire più formati di pixel e dimensioni dei file.