Clase System.Threading.Tasks.TaskScheduler

En este artículo se proporcionan comentarios adicionales a la documentación de referencia de esta API.

La TaskScheduler clase representa un programador de tareas. Un programador de tareas asegura que se ejecuta el trabajo de una tarea.

El programador de tareas predeterminado proporciona el robo de trabajo para el equilibrio de carga, la inserción o retirada de subprocesos para el rendimiento máximo y el buen rendimiento general. Debería ser suficiente para la mayoría de los escenarios.

La TaskScheduler clase también actúa como punto de extensión para toda la lógica de programación personalizable. Esto incluye mecanismos como cómo programar una tarea para su ejecución y cómo se deben exponer las tareas programadas a los depuradores. Si necesita una funcionalidad especial, puede crear un programador personalizado y habilitarlo para tareas o consultas específicas.

El programador de tareas predeterminado y el grupo de subprocesos

El programador predeterminado para la biblioteca paralela de tareas y PLINQ usa el grupo de subprocesos de .NET, representado por la ThreadPool clase , para poner en cola y ejecutar el trabajo. El grupo de subprocesos usa la información proporcionada por el Task tipo para admitir eficazmente el paralelismo específico (unidades de trabajo de corta duración) que suelen representar tareas y consultas paralelas.

Cola global frente a colas locales

El grupo de subprocesos mantiene una cola de trabajo fiFO global (primera entrada y primera salida) para subprocesos en cada dominio de aplicación. Cada vez que un programa llama al ThreadPool.QueueUserWorkItem método (o ThreadPool.UnsafeQueueUserWorkItem), el trabajo se coloca en esta cola compartida y finalmente se desintela en el siguiente subproceso que está disponible. A partir de .NET Framework 4, esta cola usa un algoritmo sin bloqueo que se parece a la ConcurrentQueue<T> clase . Mediante este uso de esta implementación sin bloqueo, el grupo de subprocesos pasa menos tiempo cuando pone en cola y desenlaca los elementos de trabajo. Esta ventaja de rendimiento está disponible para todos los programas que usan el grupo de subprocesos.

Las tareas de nivel superior, que son tareas que no se crean en el contexto de otra tarea, se colocan en la cola global igual que cualquier otro elemento de trabajo. Sin embargo, las tareas anidadas o secundarias, que se crean en el contexto de otra tarea, se controlan de forma bastante distinta. Una tarea secundaria o anidada se coloca en una cola local que es específica del subproceso en el que la tarea primaria se está ejecutando. La tarea primaria puede ser una tarea de nivel superior o también puede ser el elemento secundario de otra tarea. Cuando este subproceso está listo para más trabajo, primero busca en la cola local. Si hay elementos de trabajo esperando, se puede tener acceso a ellos rápidamente. Se tiene acceso a las colas locales en el último orden de entrada y salida (LIFO) para conservar la localidad de la caché y reducir la contención. Para obtener más información sobre las tareas secundarias y las tareas anidadas, vea Tareas secundarias adjuntas y desasociadas.

El uso de colas locales no solo reduce la presión sobre la cola global, sino que también aprovecha la localidad de datos. Los elementos de trabajo de la cola local hacen referencia a las estructuras de datos que están físicamente cerca entre sí en la memoria. En estos casos, los datos ya están en la memoria caché después de que se haya ejecutado la primera tarea y se pueda acceder a ellos rápidamente. Tanto Parallel LINQ (PLINQ) como la Parallel clase usan tareas anidadas y tareas secundarias ampliamente, y logran importantes velocidades mediante las colas de trabajo locales.

Robo de trabajo

A partir de .NET Framework 4, el grupo de subprocesos también incluye un algoritmo de robo de trabajo para ayudar a asegurarse de que no hay subprocesos inactivos mientras otros todavía tienen trabajo en sus colas. Cuando un subproceso ThreadPool está listo para más trabajo, examina primero el encabezado de la cola local, a continuación, en la cola global y después en las colas locales de otros subprocesos. Si encuentra un elemento de trabajo en la cola local de otro subproceso, aplica primero heurística para asegurarse de que puede ejecutar el trabajo eficazmente. Si es posible, desconecta el elemento de trabajo de la cola (en orden FIFO). Esto reduce la contención en cada cola local y mantiene la situación de los datos. Esta arquitectura ayuda al equilibrio de carga del grupo de subprocesos a funcionar de forma más eficaz que las versiones anteriores.

Tareas de larga duración

Tal vez le interese evitar explícitamente que una tarea se coloque en una cola local. Por ejemplo, puede saber que un elemento de trabajo determinado se ejecutará durante un tiempo relativamente largo y es probable que bloquee el resto de los elementos de trabajo de la cola local. En este caso, puede especificar la opción System.Threading.Tasks.TaskCreationOptions, que proporciona una sugerencia al programador que le indica que tal vez es necesario un subproceso adicional para que la tarea no bloquee el progreso de otros subproceso o elementos de trabajo de la cola local. Con esta opción se evita completamente el grupo de subprocesos, incluidas las colas globales y locales.

Inserción de tareas

En algunos casos en los que se espera, Task se puede ejecutar de forma sincrónica en el subproceso que realiza la operación de espera. Esto mejora el rendimiento evitando la necesidad de un subproceso adicional y, en su lugar, el uso del subproceso existente, que habría bloqueado de otro modo. Para evitar errores debido a la reentrada, la inserción de tareas solo se produce cuando el destino de espera se encuentra en la cola local del subproceso correspondiente.

Especificar un contexto de sincronización

Puede utilizar el método TaskScheduler.FromCurrentSynchronizationContext para especificar que una tarea se debería programar para ejecutarse en un subproceso determinado. Esto es útil en marcos como Windows Forms y Windows Presentation Foundation, donde el acceso a los objetos de interfaz de usuario está restringido a menudo para el código que se está ejecutando en el mismo subproceso en el que se creó el objeto UI.

En el ejemplo siguiente se usa el TaskScheduler.FromCurrentSynchronizationContext método en una aplicación de Windows Presentation Foundation (WPF) para programar una tarea en el mismo subproceso en el que se creó el control de interfaz de usuario (UI). En el ejemplo se crea un mosaico de imágenes seleccionadas aleatoriamente desde un directorio especificado. Los objetos WPF se usan para cargar y cambiar el tamaño de las imágenes. A continuación, los píxeles sin procesar se pasan a una tarea que usa un For bucle para escribir los datos de píxeles en una matriz de bytes único grande. No se requiere sincronización porque no hay dos iconos que ocupen los mismos elementos de matriz. Los iconos también se pueden escribir en cualquier orden porque su posición se calcula independientemente de cualquier otro icono. A continuación, la matriz grande se pasa a una tarea que se ejecuta en el subproceso de la interfaz de usuario, donde los datos de píxeles se cargan en un control Image.

En el ejemplo se mueven los datos fuera del subproceso de la interfaz de usuario, se modifica mediante bucles y Task objetos paralelos y, a continuación, se vuelven a pasar a una tarea que se ejecuta en el subproceso de la interfaz de usuario. Este enfoque es útil cuando tiene que usar la biblioteca paralela de tareas para realizar operaciones que no son compatibles con la API de WPF o que no son lo suficientemente rápidas. Otra manera de crear un mosaico de imágenes en WPF es usar un System.Windows.Controls.WrapPanel control y agregarle imágenes. WrapPanel Controla el trabajo de colocar los mosaicos. Sin embargo, este trabajo solo se puede realizar en el subproceso de la interfaz de usuario.

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

Para crear el ejemplo, cree un proyecto de aplicación WPF en Visual Studio y asígnelo el nombre WPF_CS1 (para un proyecto WPF de C#) o WPF_VB1 (para un proyecto de WPF de Visual Basic). A continuación, haga lo siguiente:

  1. En la vista de diseño, arrastre un Image control desde el Cuadro de herramientas a la esquina superior izquierda de la superficie de diseño. En el cuadro de texto Nombre de la ventana Propiedades , asigne al control el nombre "image".

  2. Arrastre un Button control desde el Cuadro de herramientas hasta la parte inferior izquierda de la ventana de la aplicación. En la vista XAML, especifique la Content propiedad del botón como "Crear un mosaico" y especifique su Width propiedad como "100". Conectar el Click evento con el button_Click controlador de eventos definido en el código del ejemplo agregando Click="button_Click" al <Button> elemento . En el cuadro de texto Nombre de la ventana Propiedades , asigne al control el nombre "button".

  3. Reemplace todo el contenido del archivo MainWindow.xaml.cs o MainWindow.xaml.vb por el código de este ejemplo. Para un proyecto de WPF de C#, asegúrese de que el nombre del área de trabajo coincide con el nombre del proyecto.

  4. En el ejemplo se leen imágenes JPEG de un directorio denominado C:\Users\Public\Pictures\Sample Pictures. Cree el directorio y coloque algunas imágenes en él o cambie la ruta de acceso para hacer referencia a otro directorio que contiene imágenes.

Este ejemplo tiene algunas limitaciones. Por ejemplo, solo se admiten imágenes de 32 bits por píxel; Las imágenes en otros formatos están dañadas por el BitmapImage objeto durante la operación de cambio de tamaño. Además, las imágenes de origen deben ser mayores que el tamaño del icono. Como ejercicio adicional, puede agregar funcionalidad para controlar varios formatos de píxeles y tamaños de archivo.