Udostępnij za pomocą


Klasa System.Threading.Tasks.TaskScheduler

Ten artykuł zawiera dodatkowe uwagi dotyczące dokumentacji referencyjnej dla tego interfejsu API.

Klasa TaskScheduler reprezentuje harmonogram zadań. Harmonogram zadań gwarantuje, że praca zadania zostanie ostatecznie wykonana.

Domyślny harmonogram zadań zapewnia kradzież pracy na potrzeby równoważenia obciążenia, iniekcji wątku/wycofywania w celu uzyskania maksymalnej przepływności oraz ogólnej dobrej wydajności. Powinno to być wystarczające w przypadku większości scenariuszy.

Klasa TaskScheduler służy również jako punkt rozszerzenia dla wszystkich dostosowywalnych logiki planowania. Obejmuje to mechanizmy, takie jak planowanie zadania pod kątem wykonywania oraz sposób uwidaczniania zaplanowanych zadań dla debugerów. Jeśli potrzebujesz specjalnej funkcjonalności, możesz utworzyć niestandardowy harmonogram i włączyć go dla określonych zadań lub zapytań.

Domyślny harmonogram zadań i pula wątków

Domyślny harmonogram dla biblioteki równoległej zadań i PLINQ używa puli wątków platformy .NET reprezentowanej przez klasę ThreadPool do kolejkowania i wykonywania pracy. Pula wątków używa informacji dostarczanych przez Task typ, aby efektywnie obsługiwać szczegółowe równoległości (krótkotrwałe jednostki pracy), które często reprezentują zadania równoległe i zapytania.

Kolejka globalna a kolejki lokalne

Pula wątków utrzymuje globalną kolejkę zadań typu FIFO (pierwsze weszło, pierwsze wyszło) dla wątków w każdej domenie aplikacji. Za każdym razem, gdy program wywołuje metodę ThreadPool.QueueUserWorkItem (lub ThreadPool.UnsafeQueueUserWorkItem), praca jest umieszczana w tej współdzielonej kolejce i ostatecznie przenoszona do następnego wątku, który stanie się dostępny. Począwszy od programu .NET Framework 4, ta kolejka używa algorytmu bez blokady przypominającego klasę ConcurrentQueue<T> . Dzięki tej implementacji bez blokady pula wątków poświęca mniej czasu, gdy kolejkuje i usuwa elementy robocze. Ta korzyść z wydajności jest dostępna dla wszystkich programów korzystających z puli wątków.

Zadania najwyższego poziomu, które są zadaniami, które nie są tworzone w kontekście innego zadania, są umieszczane w kolejce globalnej tak samo jak w przypadku każdego innego elementu roboczego. Jednak zagnieżdżone lub podrzędne zadania, które są tworzone w kontekście innego zadania, są obsługiwane zupełnie inaczej. Podrzędne lub zagnieżdżone zadanie jest umieszczane w lokalnej kolejce specyficznej dla wątku, na którym jest wykonywane zadanie nadrzędne. Zadanie nadrzędne może być zadaniem najwyższego poziomu lub może być również elementem podrzędnym innego zadania. Gdy ten wątek jest gotowy do dalszej pracy, najpierw patrzy w kolejkę lokalną. Jeśli elementy robocze oczekują tam, można uzyskać do nich szybki dostęp. Dostęp do kolejek lokalnych jest uzyskiwany w kolejności ostatniego, pierwszego wyjścia (LIFO), aby zachować lokalność pamięci podręcznej i zmniejszyć rywalizację. Aby uzyskać więcej informacji o zadaniach podrzędnych oraz zadaniach zagnieżdżonych, zobacz Zadania podrzędne połączone i odłączone.

Korzystanie z kolejek lokalnych nie tylko zmniejsza presję na kolejkę globalną, ale także wykorzystuje lokalność danych. Elementy robocze w kolejce lokalnej często odwołują się do struktur danych, które są fizycznie blisko siebie w pamięci. W takich przypadkach dane są już w pamięci podręcznej po uruchomieniu pierwszego zadania i można uzyskać do nich szybki dostęp. Zarówno Parallel LINQ (PLINQ), jak i Parallel klasa intensywnie używają zagnieżdżonych zadań i zadań podrzędnych, a także osiągają znaczne przyspieszenie przy użyciu lokalnych kolejek roboczych.

Kradzież pracy

Począwszy od .NET Framework 4, pula wątków obejmuje również algorytm przechwytywania pracy, aby upewnić się, że żadne wątki nie pozostają bezczynne, podczas gdy inne nadal mają pracę w swoich kolejkach. Gdy wątek puli wątków jest gotowy do dalszej pracy, najpierw sprawdza początek swojej kolejki lokalnej, następnie kolejkę globalną, a na końcu lokalne kolejki innych wątków. Jeśli znajdzie zadanie w lokalnej kolejce innego wątku, najpierw stosuje heurystyki, aby upewnić się, że może wykonać zadanie wydajnie. Jeśli to możliwe, usuwa element roboczy z kolejki z końca (w kolejności FIFO). Zmniejsza to rywalizację o każdą kolejkę lokalną i zachowuje lokalność danych. Ta architektura umożliwia lepsze równoważenie obciążenia w puli wątków niż było to w poprzednich wersjach.

Długotrwałe zadania

Możesz jawnie uniemożliwić umieszczanie zadania w kolejce lokalnej. Na przykład możesz wiedzieć, że określony element roboczy będzie uruchamiany przez stosunkowo długi czas i prawdopodobnie zablokuje wszystkie inne elementy robocze w kolejce lokalnej. W takim przypadku można określić opcję System.Threading.Tasks.TaskCreationOptions, która zawiera wskazówkę harmonogramowi, że dla zadania może być wymagany dodatkowy wątek, aby zadanie nie blokowało postępu innych wątków ani elementów roboczych w kolejce lokalnej. Korzystając z tej opcji, całkowicie unikasz puli wątków, w tym kolejek globalnych i lokalnych.

Podkreślenie zadania

W niektórych przypadkach, gdy czeka się na element Task, może być on wykonywany synchronicznie na wątku wykonującym operację oczekiwania. Zwiększa to wydajność, zapobiegając konieczności użycia dodatkowego wątku i zamiast tego wykorzystując istniejący wątek, który inaczej zostałby zablokowany. Aby zapobiec błędom spowodowanym ponowną wejściowością, włączanie zadań występuje tylko wtedy, gdy docelowe zadanie oczekiwania zostanie znalezione w lokalnej kolejce odpowiedniego wątku.

Określanie kontekstu synchronizacji

Można użyć metody TaskScheduler.FromCurrentSynchronizationContext, aby określić, że zadanie powinno być zaplanowane do uruchomienia na określonym wątku. Jest to przydatne w strukturach, takich jak Windows Forms i Windows Presentation Foundation, gdzie dostęp do obiektów interfejsu użytkownika jest często ograniczony do kodu działającego w tym samym wątku, w którym utworzono obiekt interfejsu użytkownika.

W poniższym przykładzie użyto metody TaskScheduler.FromCurrentSynchronizationContext w aplikacji Windows Presentation Foundation (WPF), aby zaplanować zadanie w tym samym wątku, w którym utworzono kontrolkę interfejsu użytkownika. W przykładzie tworzona jest mozaika obrazów, które są losowo wybierane z określonego katalogu. Obiekty WPF są używane do ładowania i zmieniania rozmiaru obrazów. Nieprzetworzone piksele są następnie przekazywane do zadania, które używa For pętli do zapisywania danych pikseli w dużej tablicy jednobajtowej. Synchronizacja nie jest wymagana, ponieważ żadne z dwóch kafelków nie zajmuje tych samych elementów w tablicy. Kafelki można również zapisywać w dowolnej kolejności, ponieważ ich położenie jest obliczane niezależnie od dowolnego innego kafelka. Duża tablica jest następnie przekazywana do zadania uruchamianego w wątku interfejsu użytkownika, w którym dane pikseli są ładowane do kontrolki obrazu.

Przykład przenosi dane z wątku interfejsu użytkownika, modyfikuje je przy użyciu pętli równoległych i Task obiektów, a następnie przekazuje je z powrotem do zadania uruchamianego w wątku interfejsu użytkownika. Takie podejście jest przydatne, gdy musisz użyć biblioteki równoległej zadań do wykonywania operacji, które nie są obsługiwane przez interfejs API WPF lub nie są wystarczająco szybkie. Innym sposobem utworzenia mozaiki obrazu w WPF jest użycie System.Windows.Controls.WrapPanel kontrolki i dodanie do niej obrazów. WrapPanel zajmuje się pracą związaną z pozycjonowaniem kafelków. Tę pracę można jednak wykonać tylko w wątku interfejsu użytkownika.

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

Aby utworzyć przykład, utwórz projekt aplikacji WPF w programie Visual Studio i nadaj mu nazwę WPF_CS1 (dla projektu C# WPF) lub WPF_VB1 (dla projektu Visual Basic WPF). Następnie wykonaj poniższe czynności:

  1. W widoku projektu przeciągnij kontrolkę Image z Toolbox do lewego górnego rogu obszaru projektowego. W polu tekstowym Nazwa okna Właściwości nadaj kontrolce nazwę "image".

  2. Przeciągnij kontrolkę Button z przybornika do lewej dolnej części okna aplikacji. W widoku XAML określ Content właściwość przycisku jako "Utwórz mozaikę" i określ jej Width właściwość jako "100". Połącz zdarzenie Click z obsługą zdarzeń button_Click zdefiniowaną w kodzie przykładu, dodając Click="button_Click" do elementu <Button>. W polu tekstowym Nazwa okna Właściwości nadaj kontrolce nazwę "button".

  3. Zastąp całą zawartość pliku MainWindow.xaml.cs lub MainWindow.xaml.vb kodem z tego przykładu. W przypadku projektu WPF języka C# upewnij się, że nazwa obszaru roboczego jest zgodna z nazwą projektu.

  4. Przykład odczytuje obrazy JPEG z katalogu o nazwie C:\Users\Public\Pictures\Sample Pictures. Utwórz katalog i umieść w nim kilka obrazów lub zmień ścieżkę, aby odwołać się do innego katalogu zawierającego obrazy.

Ten przykład ma pewne ograniczenia. Na przykład obsługiwane są tylko obrazy 32-bitowe na piksel; obrazy w innych formatach są uszkodzone przez BitmapImage obiekt podczas operacji zmiany rozmiaru. Ponadto każdy obraz źródłowy musi być większy niż rozmiar kafelka. W kolejnym ćwiczeniu możesz dodać funkcje do obsługi wielu formatów pikseli i rozmiarów plików.