Aufgabenbasiertes asynchrones Programmieren

Die Task Parallel Library (TPL) basiert auf dem Konzept einer Aufgabe, die einen asynchronen Vorgang darstellt. In mancher Hinsicht ist eine Aufgabe vergleichbar mit einem Thread- oder ThreadPool-Arbeitselement, weist jedoch ein höheres Abstraktionsniveau auf. Der Begriff Aufgabenparallelität bezeichnet eine oder mehrere eigenständige Aufgaben, die gleichzeitig ausgeführt werden. Aufgaben bieten zwei Hauptvorteile:

  • Effiziente und skalierbare Verwendung von Systemressourcen.

    Aufgaben werden im Hintergrund in den ThreadPool eingestellt, der mit Algorithmen verbessert wurde, durch die die Anzahl von Threads bestimmt und angepasst wird. Diese Algorithmen bieten einen Lastenausgleich, um den Durchsatz zu maximieren. Aufgaben sind durch diesen Prozess relativ einfach strukturiert, und Sie können für eine differenzierte Parallelität viele Aufgaben erstellen.

  • Stärker programmgesteuerte Kontrolle als bei Threads oder Arbeitselementen.

    Aufgaben und das diese umgebende Framework stellen einen umfangreichen Satz von APIs bereit, die Warten, Abbruch, Fortsetzungen, robuste Ausnahmebehandlung, detaillierte Zustandsangaben, benutzerdefinierte Planung und Vieles mehr unterstützen.

Aus beiden Gründen ist die TPL die bevorzugte API zum Schreiben von asynchronem Code, parallelem Code und Multithreadcode in .NET.

Implizites Erstellen und Ausführen von Aufgaben

Die Parallel.Invoke-Methode bietet eine einfache Möglichkeit, eine beliebige Anzahl willkürlicher Anweisungen gleichzeitig auszuführen. Sie müssen nur einen Action-Delegaten für jede Arbeitsaufgabe übergeben. Die einfachste Möglichkeit, diese Delegaten zu erstellen, sind Lambdaausdrücke. Der Lambdaausdruck kann entweder eine benannte Methode aufrufen oder den Code inline bereitstellen. Im folgenden Beispiel wird ein grundlegender Invoke-Aufruf dargestellt, der zwei Aufgaben erstellt und startet, die gleichzeitig ausgeführt werden. Die erste Aufgabe wird durch einen Lambda-Ausdruck dargestellt, der die DoSomeWork-Methode aufruft, und die zweite Aufgabe wird durch einen Lambda-Ausdruck dargestellt, der die DoSomeOtherWork-Methode aufruft.

Hinweis

Diese Dokumentation definiert Delegaten in TPL mithilfe von Lambdaausdrücken. Falls Sie mit Lambdaausdrücken in C# oder Visual Basic nicht vertraut sind, finden Sie entsprechende Informationen unter Lambdaausdrücke in PLINQ und TPL.

Parallel.Invoke(() => DoSomeWork(), () => DoSomeOtherWork());
Parallel.Invoke(Sub() DoSomeWork(), Sub() DoSomeOtherWork())

Hinweis

Die Anzahl von Task-Instanzen, die im Hintergrund von Invoke erstellt werden, ist nicht notwendigerweise mit der Anzahl der bereitgestellten Delegaten identisch. Die TPL kann verschiedene Optimierungen einsetzen, besonders bei einer großen Anzahl von Delegaten.

Weitere Informationen finden Sie unter Vorgehensweise: Ausführen von parallelen Vorgängen mithilfe von „Parallel.Invoke“.

Wenn Sie die Aufgabenausführung präziser steuern oder einen Wert aus der Aufgabe zurückgeben möchten, müssen Sie expliziter mit Task-Objekten arbeiten.

Explizites Erstellen und Ausführen von Aufgaben

Eine Aufgabe, die keinen Wert zurückgibt, wird durch die System.Threading.Tasks.Task-Klasse dargestellt. Eine Aufgabe, die einen Wert zurückgibt, wird durch die System.Threading.Tasks.Task<TResult>-Klasse dargestellt, die von Task erbt. Das Aufgabenobjekt verarbeitet die Infrastrukturdetails und stellt Methoden sowie Eigenschaften bereit, auf die während der gesamten Lebensdauer der Aufgabe vom aufrufenden Thread aus zugegriffen werden kann. Sie können beispielsweise jederzeit auf die Status-Eigenschaft einer Aufgabe zugreifen, um zu ermitteln, ob die Ausführung gestartet, abgeschlossen oder abgebrochen wurde bzw. ob eine Ausnahme ausgelöst wurde. Der Status wird durch eine TaskStatus-Enumeration dargestellt.

Wenn Sie eine Aufgabe erstellen, weisen Sie dieser einen Benutzerdelegaten zu, der den von der Aufgabe ausgeführten Code kapselt. Der Delegat kann als benannter Delegat, als anonyme Methode oder als Lambda-Ausdruck angegeben werden. Lambdaausdrücke können einen Aufruf einer benannten Methode enthalten, wie im folgenden Beispiel gezeigt. Das Beispiel enthält einen Aufruf der Task.Wait-Methode, um sicherzustellen, dass die Aufgabe abgeschlossen ist, ehe die Konsolenmodusanwendung beendet wird.

using System;
using System.Threading;
using System.Threading.Tasks;

public class Lambda
{
   public static void Main()
   {
      Thread.CurrentThread.Name = "Main";

      // Create a task and supply a user delegate by using a lambda expression.
      Task taskA = new Task( () => Console.WriteLine("Hello from taskA."));
      // Start the task.
      taskA.Start();

      // Output a message from the calling thread.
      Console.WriteLine("Hello from thread '{0}'.",
                        Thread.CurrentThread.Name);
      taskA.Wait();
   }
}
// The example displays output as follows:
//       Hello from thread 'Main'.
//       Hello from taskA.
// or
//       Hello from taskA.
//       Hello from thread 'Main'.
Imports System.Threading

Namespace Lambda
    Module Example
        Public Sub Main()
            Thread.CurrentThread.Name = "Main"

            ' Create a task and supply a user delegate by using a lambda expression. 
            Dim taskA = New Task(Sub() Console.WriteLine("Hello from taskA."))
            ' Start the task.
            taskA.Start()

            ' Output a message from the calling thread.
            Console.WriteLine("Hello from thread '{0}'.",
                            Thread.CurrentThread.Name)
            taskA.Wait()
        End Sub
    End Module
    ' The example displays output like the following:
    '    Hello from thread 'Main'.
    '    Hello from taskA.
End Namespace

Sie können auch die Task.Run-Methoden verwenden, um eine Aufgabe in einem Vorgang zu erstellen und zu starten. Zum Verwalten der Aufgabe verwenden die Run-Methoden den Standardaufgabenplaner, unabhängig von dem Aufgabenplaner, der dem aktuellen Thread zugewiesen ist. Wenn eine präzisere Steuerung der Erstellung und Planung von Aufgaben nicht erforderlich ist, sollten diese vorzugsweise mit den Run-Methoden erstellt und gestartet werden.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Run;

public class Example
{
   public static void Main()
   {
      Thread.CurrentThread.Name = "Main";

      // Define and run the task.
      Task taskA = Task.Run( () => Console.WriteLine("Hello from taskA."));

      // Output a message from the calling thread.
      Console.WriteLine("Hello from thread '{0}'.",
                          Thread.CurrentThread.Name);
      taskA.Wait();
   }
}
// The example displays output as follows:
//       Hello from thread 'Main'.
//       Hello from taskA.
// or
//       Hello from taskA.
//       Hello from thread 'Main'.
Imports System.Threading

Namespace Run
    Module Example
        Public Sub Main()
            Thread.CurrentThread.Name = "Main"

            Dim taskA As Task = Task.Run(Sub() Console.WriteLine("Hello from taskA."))

            ' Output a message from the calling thread.
            Console.WriteLine("Hello from thread '{0}'.",
                            Thread.CurrentThread.Name)
            taskA.Wait()
        End Sub
    End Module
    ' The example displays output like the following:
    '    Hello from thread 'Main'.
    '    Hello from taskA.
End Namespace

Sie können auch die TaskFactory.StartNew-Methode verwenden, um eine Aufgabe in einem Schritt zu erstellen und zu starten. Wie im folgenden Beispiel gezeigt, können Sie diese Methode in diesen Fällen verwenden:

  • Erstellung und Planung müssen nicht getrennt werden, und Sie benötigen zusätzliche Aufgabenerstellungsoptionen oder die Verwendung eines bestimmten Planers.

  • Sie müssen zusätzlichen Zustand an die Aufgabe übergeben, den Sie über die Task.AsyncState-Eigenschaft abrufen können.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TaskIntro;

class CustomData
{
    public long CreationTime;
    public int Name;
    public int ThreadNum;
}

public class AsyncState
{
    public static void Main()
    {
        Task[] taskArray = new Task[10];
        for (int i = 0; i < taskArray.Length; i++)
        {
            taskArray[i] = Task.Factory.StartNew((Object obj) =>
            {
                CustomData data = obj as CustomData;
                if (data == null) return;

                data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
            },
            new CustomData() { Name = i, CreationTime = DateTime.Now.Ticks });
        }
        Task.WaitAll(taskArray);
        foreach (var task in taskArray)
        {
            var data = task.AsyncState as CustomData;
            if (data != null)
                Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
                                  data.Name, data.CreationTime, data.ThreadNum);
        }
    }
}
// The example displays output like the following:
//     Task #0 created at 635116412924597583, ran on thread #3.
//     Task #1 created at 635116412924607584, ran on thread #4.
//     Task #2 created at 635116412924607584, ran on thread #4.
//     Task #3 created at 635116412924607584, ran on thread #4.
//     Task #4 created at 635116412924607584, ran on thread #3.
//     Task #5 created at 635116412924607584, ran on thread #3.
//     Task #6 created at 635116412924607584, ran on thread #4.
//     Task #7 created at 635116412924607584, ran on thread #4.
//     Task #8 created at 635116412924607584, ran on thread #3.
//     Task #9 created at 635116412924607584, ran on thread #4.
Imports System.Threading

Namespace AsyncState
    Class CustomData
        Public CreationTime As Long
        Public Name As Integer
        Public ThreadNum As Integer
    End Class

    Module Example
        Public Sub Main()
            Dim taskArray(9) As Task
            For i As Integer = 0 To taskArray.Length - 1
                taskArray(i) = Task.Factory.StartNew(Sub(obj As Object)
                                                         Dim data As CustomData = TryCast(obj, CustomData)
                                                         If data Is Nothing Then Return

                                                         data.ThreadNum = Environment.CurrentManagedThreadId
                                                     End Sub,
                New CustomData With {.Name = i, .CreationTime = Date.Now.Ticks})
            Next
            Task.WaitAll(taskArray)

            For Each task In taskArray
                Dim data = TryCast(task.AsyncState, CustomData)
                If data IsNot Nothing Then
                    Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
                                    data.Name, data.CreationTime, data.ThreadNum)
                End If
            Next
        End Sub
    End Module
    ' The example displays output like the following:
    '     Task #0 created at 635116412924597583, ran on thread #3.
    '     Task #1 created at 635116412924607584, ran on thread #4.
    '     Task #2 created at 635116412924607584, ran on thread #4.
    '     Task #3 created at 635116412924607584, ran on thread #4.
    '     Task #4 created at 635116412924607584, ran on thread #3.
    '     Task #5 created at 635116412924607584, ran on thread #3.
    '     Task #6 created at 635116412924607584, ran on thread #4.
    '     Task #7 created at 635116412924607584, ran on thread #4.
    '     Task #8 created at 635116412924607584, ran on thread #3.
    '     Task #9 created at 635116412924607584, ran on thread #4.
End Namespace

Task und Task<TResult> machen jeweils eine statische Factory-Eigenschaft verfügbar, von der eine Standardinstanz von TaskFactory zurückgegeben wird, sodass Sie die Methode als Task.Factory.StartNew() aufrufen können. Darüber hinaus verfügen die Aufgaben im Beispiel, da sie vom System.Threading.Tasks.Task<TResult>-Typ sind, jeweils über eine öffentliche Task<TResult>.Result-Eigenschaft, die das Ergebnis der Berechnung enthält. Die Aufgaben werden asynchron ausgeführt und können in einer beliebigen Reihenfolge abgeschlossen werden. Wenn auf die Result-Eigenschaft zugegriffen wird, ehe die Berechnung beendet wurde, sperrt die Eigenschaft den aufrufenden Thread, bis der Wert verfügbar ist.

using System;
using System.Threading.Tasks;

public class Result
{
   public static void Main()
   {
        Task<Double>[] taskArray = { Task<Double>.Factory.StartNew(() => DoComputation(1.0)),
                                     Task<Double>.Factory.StartNew(() => DoComputation(100.0)),
                                     Task<Double>.Factory.StartNew(() => DoComputation(1000.0)) };

        var results = new Double[taskArray.Length];
        Double sum = 0;

        for (int i = 0; i < taskArray.Length; i++) {
            results[i] = taskArray[i].Result;
            Console.Write("{0:N1} {1}", results[i],
                              i == taskArray.Length - 1 ? "= " : "+ ");
            sum += results[i];
        }
        Console.WriteLine("{0:N1}", sum);
   }

   private static Double DoComputation(Double start)
   {
      Double sum = 0;
      for (var value = start; value <= start + 10; value += .1)
         sum += value;

      return sum;
   }
}
// The example displays the following output:
//        606.0 + 10,605.0 + 100,495.0 = 111,706.0

Namespace Result
    Module Example
        Public Sub Main()
            Dim taskArray() = {Task(Of Double).Factory.StartNew(Function() DoComputation(1.0)),
                Task(Of Double).Factory.StartNew(Function() DoComputation(100.0)),
                Task(Of Double).Factory.StartNew(Function() DoComputation(1000.0))}

            Dim results(taskArray.Length - 1) As Double
            Dim sum As Double

            For i As Integer = 0 To taskArray.Length - 1
                results(i) = taskArray(i).Result
                Console.Write("{0:N1} {1}", results(i),
                    If(i = taskArray.Length - 1, "= ", "+ "))
                sum += results(i)
            Next
            Console.WriteLine("{0:N1}", sum)
        End Sub

        Private Function DoComputation(start As Double) As Double
            Dim sum As Double
            For value As Double = start To start + 10 Step .1
                sum += value
            Next
            Return sum
        End Function
    End Module
    ' The example displays the following output:
    '       606.0 + 10,605.0 + 100,495.0 = 111,706.0
End Namespace

Weitere Informationen finden Sie unter Vorgehensweise: Zurückgeben eines Werts aus einer Aufgabe.

Wenn Sie einen Lambdaausdruck verwenden, um einen Delegaten zu erstellen, haben Sie Zugriff auf alle Variablen, die an dieser Stelle im Quellcode sichtbar sind. In einigen Fällen jedoch, insbesondere in Schleifen, wird die Variable nicht wie erwartet vom Lambda-Ausdruck erfasst. Es wird nur der Verweis auf die Variable erfasst, nicht der Wert, da der sich mit jeder Iteration ändert. Das Problem wird anhand des folgenden Beispiels veranschaulicht. Es wird ein Schleifenzähler an den Lambda-Ausdruck übergeben, wodurch ein CustomData-Objekt instanziiert wird und der Schleifenzähler als Objektbezeichner verwendet. Die Ausgabe im Beispiel zeigt, dass jedes CustomData-Objekt einen identischen Bezeichner hat.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Example.Iterations;

class CustomData
{
   public long CreationTime;
   public int Name;
   public int ThreadNum;
}

public class IterationTwo
{
   public static void Main()
   {
      // Create the task object by using an Action(Of Object) to pass in the loop
      // counter. This produces an unexpected result.
      Task[] taskArray = new Task[10];
      for (int i = 0; i < taskArray.Length; i++) {
         taskArray[i] = Task.Factory.StartNew( (Object obj) => {
                                                 var data = new CustomData() {Name = i, CreationTime = DateTime.Now.Ticks};
                                                 data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
                                                 Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
                                                                   data.Name, data.CreationTime, data.ThreadNum);
                                               },
                                              i );
      }
      Task.WaitAll(taskArray);
   }
}
// The example displays output like the following:
//       Task #10 created at 635116418427727841 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427727841 on thread #3.
//       Task #10 created at 635116418427747843 on thread #3.
//       Task #10 created at 635116418427747843 on thread #3.
//       Task #10 created at 635116418427737842 on thread #4.
Imports System.Threading

Namespace IterationsTwo
    Class CustomData
        Public CreationTime As Long
        Public Name As Integer
        Public ThreadNum As Integer
    End Class

    Module Example
        Public Sub Main()
            ' Create the task object by using an Action(Of Object) to pass in the loop
            ' counter. This produces an unexpected result.
            Dim taskArray(9) As Task
            For i As Integer = 0 To taskArray.Length - 1
                taskArray(i) = Task.Factory.StartNew(Sub(obj As Object)
                                                         Dim data As New CustomData With {.Name = i, .CreationTime = Date.Now.Ticks}
                                                         data.ThreadNum = Environment.CurrentManagedThreadId
                                                         Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
                                                                         data.Name, data.CreationTime, data.ThreadNum)
                                                     End Sub,
                    i)
            Next
            Task.WaitAll(taskArray)
        End Sub
    End Module
    ' The example displays output like the following:
    '       Task #10 created at 635116418427727841 on thread #4.
    '       Task #10 created at 635116418427737842 on thread #4.
    '       Task #10 created at 635116418427737842 on thread #4.
    '       Task #10 created at 635116418427737842 on thread #4.
    '       Task #10 created at 635116418427737842 on thread #4.
    '       Task #10 created at 635116418427737842 on thread #4.
    '       Task #10 created at 635116418427727841 on thread #3.
    '       Task #10 created at 635116418427747843 on thread #3.
    '       Task #10 created at 635116418427747843 on thread #3.
    '       Task #10 created at 635116418427737842 on thread #4.
End Namespace

Sie können auf den Wert jeder Iteration zugreifen, indem Sie für die Aufgabe über ihren Konstruktor ein Zustandsobjekt bereitstellen. Im folgenden Beispiel wird das vorhergehende Beispiel geändert, indem der Schleifenzähler beim Erstellen des CustomData-Objekts verwendet wird, das wiederum an den Lambda-Ausdruck übergeben wird. Wie die Ausgabe im Beispiel zeigt, weist jedes CustomData-Objekt jetzt einen eindeutigen Bezeichner basierend auf den Wert des Schleifenzählers zum Zeitpunkt der Instanziierung des Objekts auf.

using System;
using System.Threading;
using System.Threading.Tasks;

class CustomData
{
   public long CreationTime;
   public int Name;
   public int ThreadNum;
}

public class IterationOne
{
   public static void Main()
   {
      // Create the task object by using an Action(Of Object) to pass in custom data
      // to the Task constructor. This is useful when you need to capture outer variables
      // from within a loop.
      Task[] taskArray = new Task[10];
      for (int i = 0; i < taskArray.Length; i++) {
         taskArray[i] = Task.Factory.StartNew( (Object obj ) => {
                                                  CustomData data = obj as CustomData;
                                                  if (data == null)
                                                     return;

                                                  data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
                                                  Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
                                                                   data.Name, data.CreationTime, data.ThreadNum);
                                               },
                                               new CustomData() {Name = i, CreationTime = DateTime.Now.Ticks} );
      }
      Task.WaitAll(taskArray);
   }
}
// The example displays output like the following:
//       Task #0 created at 635116412924597583 on thread #3.
//       Task #1 created at 635116412924607584 on thread #4.
//       Task #3 created at 635116412924607584 on thread #4.
//       Task #4 created at 635116412924607584 on thread #4.
//       Task #2 created at 635116412924607584 on thread #3.
//       Task #6 created at 635116412924607584 on thread #3.
//       Task #5 created at 635116412924607584 on thread #4.
//       Task #8 created at 635116412924607584 on thread #4.
//       Task #7 created at 635116412924607584 on thread #3.
//       Task #9 created at 635116412924607584 on thread #4.
Imports System.Threading

Namespace IterationsOne
    Class CustomData
        Public CreationTime As Long
        Public Name As Integer
        Public ThreadNum As Integer
    End Class

    Module Example
        Public Sub Main()
            ' Create the task object by using an Action(Of Object) to pass in custom data
            ' to the Task constructor. This is useful when you need to capture outer variables
            ' from within a loop. 
            Dim taskArray(9) As Task
            For i As Integer = 0 To taskArray.Length - 1
                taskArray(i) = Task.Factory.StartNew(Sub(obj As Object)
                                                         Dim data As CustomData = TryCast(obj, CustomData)
                                                         If data Is Nothing Then Return

                                                         data.ThreadNum = Environment.CurrentManagedThreadId
                                                         Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
                                                                         data.Name, data.CreationTime, data.ThreadNum)
                                                     End Sub,
                    New CustomData With {.Name = i, .CreationTime = Date.Now.Ticks})
            Next
            Task.WaitAll(taskArray)
        End Sub
    End Module
    ' The example displays output like the following:
    '       Task #0 created at 635116412924597583 on thread #3.
    '       Task #1 created at 635116412924607584 on thread #4.
    '       Task #3 created at 635116412924607584 on thread #4.
    '       Task #4 created at 635116412924607584 on thread #4.
    '       Task #2 created at 635116412924607584 on thread #3.
    '       Task #6 created at 635116412924607584 on thread #3.
    '       Task #5 created at 635116412924607584 on thread #4.
    '       Task #8 created at 635116412924607584 on thread #4.
    '       Task #7 created at 635116412924607584 on thread #3.
    '       Task #9 created at 635116412924607584 on thread #4.
End Namespace

Dieser Zustand wird als Argument an den Aufgabendelegaten übergeben, und mit der Task.AsyncState-Eigenschaft kann über das Aufgabenobjekt auf den Zustand zugegriffen werden. Das folgende Beispiel zeigt eine Abwandlung des vorherigen Beispiels. Es verwendet die AsyncState-Eigenschaft, um Informationen zu den CustomData-Objekten anzuzeigen, die an den Lambdaausdruck übergeben wurden.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TaskIntro;

class CustomData
{
    public long CreationTime;
    public int Name;
    public int ThreadNum;
}

public class AsyncState
{
    public static void Main()
    {
        Task[] taskArray = new Task[10];
        for (int i = 0; i < taskArray.Length; i++)
        {
            taskArray[i] = Task.Factory.StartNew((Object obj) =>
            {
                CustomData data = obj as CustomData;
                if (data == null) return;

                data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
            },
            new CustomData() { Name = i, CreationTime = DateTime.Now.Ticks });
        }
        Task.WaitAll(taskArray);
        foreach (var task in taskArray)
        {
            var data = task.AsyncState as CustomData;
            if (data != null)
                Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
                                  data.Name, data.CreationTime, data.ThreadNum);
        }
    }
}
// The example displays output like the following:
//     Task #0 created at 635116412924597583, ran on thread #3.
//     Task #1 created at 635116412924607584, ran on thread #4.
//     Task #2 created at 635116412924607584, ran on thread #4.
//     Task #3 created at 635116412924607584, ran on thread #4.
//     Task #4 created at 635116412924607584, ran on thread #3.
//     Task #5 created at 635116412924607584, ran on thread #3.
//     Task #6 created at 635116412924607584, ran on thread #4.
//     Task #7 created at 635116412924607584, ran on thread #4.
//     Task #8 created at 635116412924607584, ran on thread #3.
//     Task #9 created at 635116412924607584, ran on thread #4.
Imports System.Threading

Namespace AsyncState
    Class CustomData
        Public CreationTime As Long
        Public Name As Integer
        Public ThreadNum As Integer
    End Class

    Module Example
        Public Sub Main()
            Dim taskArray(9) As Task
            For i As Integer = 0 To taskArray.Length - 1
                taskArray(i) = Task.Factory.StartNew(Sub(obj As Object)
                                                         Dim data As CustomData = TryCast(obj, CustomData)
                                                         If data Is Nothing Then Return

                                                         data.ThreadNum = Environment.CurrentManagedThreadId
                                                     End Sub,
                New CustomData With {.Name = i, .CreationTime = Date.Now.Ticks})
            Next
            Task.WaitAll(taskArray)

            For Each task In taskArray
                Dim data = TryCast(task.AsyncState, CustomData)
                If data IsNot Nothing Then
                    Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
                                    data.Name, data.CreationTime, data.ThreadNum)
                End If
            Next
        End Sub
    End Module
    ' The example displays output like the following:
    '     Task #0 created at 635116412924597583, ran on thread #3.
    '     Task #1 created at 635116412924607584, ran on thread #4.
    '     Task #2 created at 635116412924607584, ran on thread #4.
    '     Task #3 created at 635116412924607584, ran on thread #4.
    '     Task #4 created at 635116412924607584, ran on thread #3.
    '     Task #5 created at 635116412924607584, ran on thread #3.
    '     Task #6 created at 635116412924607584, ran on thread #4.
    '     Task #7 created at 635116412924607584, ran on thread #4.
    '     Task #8 created at 635116412924607584, ran on thread #3.
    '     Task #9 created at 635116412924607584, ran on thread #4.
End Namespace

Aufgaben-ID

Jeder Aufgabe wird eine ganzzahlige ID zugeordnet, durch die diese in einer Anwendungsdomäne eindeutig identifiziert wird und auf die mit der Task.Id-Eigenschaft zugegriffen werden kann. Die ID vereinfacht das Anzeigen von Aufgabeninformationen in den Fenstern Parallele Stapel und Parallele Aufgaben des Visual Studio-Debuggers. Die ID wird verzögert erstellt, was bedeutet, dass sie erst erstellt wird, wenn sie angefordert wird. Daher kann eine Aufgabe bei jeder Ausführung des Programms eine andere ID aufweisen. Weitere Informationen zum Anzeigen von Aufgaben-IDs im Debugger finden Sie unter Verwenden des Fensters „Aufgaben“ und Verwenden des Fensters „Parallele Stapel“.

Aufgabenerstellungsoptionen

Die meisten APIs, die Aufgaben erstellen, stellen Überladungen bereit, die einen TaskCreationOptions-Parameter akzeptieren. Durch Angabe mindestens einer dieser Optionen teilen Sie dem Taskplaner mit, wie die Task im Threadpool geplant werden soll. Die Optionen können mit einem bitweisen OR-Vorgang kombiniert werden.

Im folgenden Beispiel wird eine Task veranschaulicht, die über die LongRunning-Option und die PreferFairness-Option verfügt:

var task3 = new Task(() => MyLongRunningMethod(),
                    TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness);
task3.Start();

Dim task3 = New Task(Sub() MyLongRunningMethod(),
                        TaskCreationOptions.LongRunning Or TaskCreationOptions.PreferFairness)
task3.Start()

Aufgaben, Threads und Kultur

Jeder Thread weist eine zugeordnete Kultur und Benutzeroberflächenkultur auf, die entsprechend durch die Thread.CurrentCulture- bzw. die Thread.CurrentUICulture-Eigenschaft definiert sind. Die Kultur eines Threads wird in Vorgängen wie Formatieren, Analysieren, Sortieren und Zeichenfolgenvergleich verwendet. Die Benutzeroberflächenkultur eines Threads wird für Nachschlagen in Ressourcen verwendet.

Die Systemkultur definiert die Standardkultur und die Benutzeroberflächenkultur eines Threads. Sie können jedoch eine Standardkultur für alle Threads in einer Anwendungsdomäne angeben, indem Sie die Eigenschaften CultureInfo.DefaultThreadCurrentCulture und CultureInfo.DefaultThreadCurrentUICulture verwenden. Wenn Sie die Kultur eines Threads explizit festlegen und einen neuen Thread starten, erbt der neue Thread nicht die Kultur des aufrufenden Threads, sondern stattdessen wird die Standardkultur des Systems als seine Kultur verwendet. Bei der taskbasierten Programmierung verwenden Tasks jedoch die Kultur des aufrufenden Threads, selbst wenn der Task in einem anderen Thread asynchron ausgeführt wird.

Das folgende Beispiel bietet eine einfache Veranschaulichung. Es ändert die aktuelle Kultur der App in Französisch (Frankreich). Wenn Französisch (Frankreich) bereits die aktuelle Kultur ist, ändert es sie in Englisch (USA). Anschließend wird der Delegat formatDelegate aufgerufen, der einige Zahlen zurückgibt, die als Währungswerte in der neuen Kultur formatiert sind. Eine Task verwendet die Kultur des aufrufenden Threads unabhängig davon, ob der Delegat von der Task synchron oder asynchron aufgerufen wird.

using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
       decimal[] values = { 163025412.32m, 18905365.59m };
       string formatString = "C2";
       Func<String> formatDelegate = () => { string output = String.Format("Formatting using the {0} culture on thread {1}.\n",
                                                                           CultureInfo.CurrentCulture.Name,
                                                                           Thread.CurrentThread.ManagedThreadId);
                                             foreach (var value in values)
                                                output += String.Format("{0}   ", value.ToString(formatString));

                                             output += Environment.NewLine;
                                             return output;
                                           };

       Console.WriteLine("The example is running on thread {0}",
                         Thread.CurrentThread.ManagedThreadId);
       // Make the current culture different from the system culture.
       Console.WriteLine("The current culture is {0}",
                         CultureInfo.CurrentCulture.Name);
       if (CultureInfo.CurrentCulture.Name == "fr-FR")
          Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
       else
          Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR");

       Console.WriteLine("Changed the current culture to {0}.\n",
                         CultureInfo.CurrentCulture.Name);

       // Execute the delegate synchronously.
       Console.WriteLine("Executing the delegate synchronously:");
       Console.WriteLine(formatDelegate());

       // Call an async delegate to format the values using one format string.
       Console.WriteLine("Executing a task asynchronously:");
       var t1 = Task.Run(formatDelegate);
       Console.WriteLine(t1.Result);

       Console.WriteLine("Executing a task synchronously:");
       var t2 = new Task<String>(formatDelegate);
       t2.RunSynchronously();
       Console.WriteLine(t2.Result);
   }
}
// The example displays the following output:
//         The example is running on thread 1
//         The current culture is en-US
//         Changed the current culture to fr-FR.
//
//         Executing the delegate synchronously:
//         Formatting using the fr-FR culture on thread 1.
//         163 025 412,32 €   18 905 365,59 €
//
//         Executing a task asynchronously:
//         Formatting using the fr-FR culture on thread 3.
//         163 025 412,32 €   18 905 365,59 €
//
//         Executing a task synchronously:
//         Formatting using the fr-FR culture on thread 1.
//         163 025 412,32 €   18 905 365,59 €
Imports System.Globalization
Imports System.Threading

Module Example
    Public Sub Main()
        Dim values() As Decimal = {163025412.32D, 18905365.59D}
        Dim formatString As String = "C2"
        Dim formatDelegate As Func(Of String) = Function()
                                                    Dim output As String = String.Format("Formatting using the {0} culture on thread {1}.",
                                                                                         CultureInfo.CurrentCulture.Name,
                                                                                         Thread.CurrentThread.ManagedThreadId)
                                                    output += Environment.NewLine
                                                    For Each value In values
                                                        output += String.Format("{0}   ", value.ToString(formatString))
                                                    Next
                                                    output += Environment.NewLine
                                                    Return output
                                                End Function

        Console.WriteLine("The example is running on thread {0}",
                          Thread.CurrentThread.ManagedThreadId)
        ' Make the current culture different from the system culture.
        Console.WriteLine("The current culture is {0}",
                          CultureInfo.CurrentCulture.Name)
        If CultureInfo.CurrentCulture.Name = "fr-FR" Then
            Thread.CurrentThread.CurrentCulture = New CultureInfo("en-US")
        Else
            Thread.CurrentThread.CurrentCulture = New CultureInfo("fr-FR")
        End If
        Console.WriteLine("Changed the current culture to {0}.",
                          CultureInfo.CurrentCulture.Name)
        Console.WriteLine()

        ' Execute the delegate synchronously.
        Console.WriteLine("Executing the delegate synchronously:")
        Console.WriteLine(formatDelegate())

        ' Call an async delegate to format the values using one format string.
        Console.WriteLine("Executing a task asynchronously:")
        Dim t1 = Task.Run(formatDelegate)
        Console.WriteLine(t1.Result)

        Console.WriteLine("Executing a task synchronously:")
        Dim t2 = New Task(Of String)(formatDelegate)
        t2.RunSynchronously()
        Console.WriteLine(t2.Result)
    End Sub
End Module

' The example displays the following output:
'
'          The example is running on thread 1
'          The current culture is en-US
'          Changed the current culture to fr-FR.
'
'          Executing the delegate synchronously:
'          Formatting Imports the fr-FR culture on thread 1.
'          163 025 412,32 €   18 905 365,59 €
'
'          Executing a task asynchronously:
'          Formatting Imports the fr-FR culture on thread 3.
'          163 025 412,32 €   18 905 365,59 €
'
'          Executing a task synchronously:
'          Formatting Imports the fr-FR culture on thread 1.
'          163 025 412,32 €   18 905 365,59 €

Hinweis

In Versionen von .NET Framework vor Version 4.6 wurde die Kultur einer Aufgabe durch die Kultur des Threads bestimmt, in dem sie ausgeführt wurde, und nicht durch die Kultur des aufrufenden Threads. Bei asynchronen Aufgaben bedeutet dies, dass sich die Kultur, die von der Aufgabe verwendet wird, von der Kultur des aufrufenden Threads abweichen könnte.

Weitere Informationen zu asynchronen Aufgaben und Kultur, finden Sie im Artikel CultureInfo im Abschnitt „Kultur und asynchrone aufgabenbasierte Vorgänge“.

Erstellen von Aufgabenfortsetzungen

Mithilfe der Methoden Task.ContinueWith und Task<TResult>.ContinueWith können Sie eine Aufgabe angeben, die gestartet werden soll, wenn die Vorgängeraufgabe abgeschlossen wurde. Dem Delegaten der Fortsetzungsaufgabe wird ein Verweis auf die Vorgängeraufgabe übergeben, damit deren Status überprüft werden kann. Wenn Sie den Wert der Task<TResult>.Result-Eigenschaft abrufen, können Sie die Ausgabe des Vorgängers als Eingabe für die Fortsetzung verwenden.

Im folgenden Beispiel wird die getData-Aufgabe durch einen Aufruf der TaskFactory.StartNew<TResult>(Func<TResult>)-Methode gestartet. Die processData-Aufgabe wird automatisch gestartet, wenn getData endet, und displayData wird gestartet, wenn processData endet. getData erzeugt ein Ganzzahlarray, auf das über die processData-Aufgabe durch die getData-Eigenschaft der Task<TResult>.Result-Aufgabe zugegriffen werden kann. Die processData Aufgabe verarbeitet das Array und gibt ein Ergebnis zurück, dessen Typ vom Rückgabetyp des Lambda-Ausdrucks abgeleitet wird, der an die Task<TResult>.ContinueWith<TNewResult>(Func<Task<TResult>,TNewResult>)-Methode übergeben wurde. Die displayData-Aufgabe wird automatisch ausgeführt, wenn processData endet und das Tuple<T1,T2,T3>-Objekt, das durch den processData-Lambda-Ausdruck zurückgegeben wurde, über die displayData-Eigenschaft der processData-Aufgabe für die Task<TResult>.Result-Aufgabe zugänglich ist. Die Aufgabe displayData übernimmt das Ergebnis der Aufgabe processData. Sie erzeugt ein Ergebnis, dessen Typ auf eine ähnliche Weise abgeleitet wird und das dem Programm in der Result-Eigenschaft zur Verfügung gestellt wird.

using System;
using System.Threading.Tasks;

public class ContinuationOne
{
   public static void Main()
   {
      var getData = Task.Factory.StartNew(() => {
                                             Random rnd = new Random();
                                             int[] values = new int[100];
                                             for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
                                                values[ctr] = rnd.Next();

                                             return values;
                                          } );
      var processData = getData.ContinueWith((x) => {
                                                int n = x.Result.Length;
                                                long sum = 0;
                                                double mean;

                                                for (int ctr = 0; ctr <= x.Result.GetUpperBound(0); ctr++)
                                                   sum += x.Result[ctr];

                                                mean = sum / (double) n;
                                                return Tuple.Create(n, sum, mean);
                                             } );
      var displayData = processData.ContinueWith((x) => {
                                                    return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
                                                                         x.Result.Item1, x.Result.Item2,
                                                                         x.Result.Item3);
                                                 } );
      Console.WriteLine(displayData.Result);
   }
}
// The example displays output similar to the following:
//    N=100, Total = 110,081,653,682, Mean = 1,100,816,536.82

Namespace ContinuationsOne
    Module Example
        Public Sub Main()
            Dim getData = Task.Factory.StartNew(Function()
                                                    Dim rnd As New Random()
                                                    Dim values(99) As Integer
                                                    For ctr = 0 To values.GetUpperBound(0)
                                                        values(ctr) = rnd.Next()
                                                    Next
                                                    Return values
                                                End Function)
            Dim processData = getData.ContinueWith(Function(x)
                                                       Dim n As Integer = x.Result.Length
                                                       Dim sum As Long
                                                       Dim mean As Double

                                                       For ctr = 0 To x.Result.GetUpperBound(0)
                                                           sum += x.Result(ctr)
                                                       Next
                                                       mean = sum / n
                                                       Return Tuple.Create(n, sum, mean)
                                                   End Function)
            Dim displayData = processData.ContinueWith(Function(x)
                                                           Return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
                                                                                   x.Result.Item1, x.Result.Item2,
                                                                                   x.Result.Item3)
                                                       End Function)
            Console.WriteLine(displayData.Result)
        End Sub
    End Module
    ' The example displays output like the following:
    '   N=100, Total = 110,081,653,682, Mean = 1,100,816,536.82
End Namespace

Da Task.ContinueWith eine Instanzmethode ist, können Sie die Methodenaufrufe verketten, anstatt ein Task<TResult>-Objekt für jede Vorgängeraufgabe zu instanziieren. Das folgende Beispiel entspricht funktionell dem vorherigen Beispiel, verkettet aber die Aufrufe der Task.ContinueWith-Methode. Das Task<TResult>-Objekt, das durch die Kette von Methodenaufrufen zurückgegeben wird, ist die endgültige Fortsetzungsaufgabe.

using System;
using System.Threading.Tasks;

public class ContinuationTwo
{
   public static void Main()
   {
      var displayData = Task.Factory.StartNew(() => {
                                                 Random rnd = new Random();
                                                 int[] values = new int[100];
                                                 for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
                                                    values[ctr] = rnd.Next();

                                                 return values;
                                              } ).
                        ContinueWith((x) => {
                                        int n = x.Result.Length;
                                        long sum = 0;
                                        double mean;

                                        for (int ctr = 0; ctr <= x.Result.GetUpperBound(0); ctr++)
                                           sum += x.Result[ctr];

                                        mean = sum / (double) n;
                                        return Tuple.Create(n, sum, mean);
                                     } ).
                        ContinueWith((x) => {
                                        return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
                                                             x.Result.Item1, x.Result.Item2,
                                                             x.Result.Item3);
                                     } );
      Console.WriteLine(displayData.Result);
   }
}
// The example displays output similar to the following:
//    N=100, Total = 110,081,653,682, Mean = 1,100,816,536.82

Namespace ContinuationsTwo
    Module Example
        Public Sub Main()
            Dim displayData = Task.Factory.StartNew(Function()
                                                        Dim rnd As New Random()
                                                        Dim values(99) As Integer
                                                        For ctr = 0 To values.GetUpperBound(0)
                                                            values(ctr) = rnd.Next()
                                                        Next
                                                        Return values
                                                    End Function). _
            ContinueWith(Function(x)
                             Dim n As Integer = x.Result.Length
                             Dim sum As Long
                             Dim mean As Double

                             For ctr = 0 To x.Result.GetUpperBound(0)
                                 sum += x.Result(ctr)
                             Next
                             mean = sum / n
                             Return Tuple.Create(n, sum, mean)
                         End Function). _
            ContinueWith(Function(x)
                             Return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
                                                 x.Result.Item1, x.Result.Item2,
                                                 x.Result.Item3)
                         End Function)
            Console.WriteLine(displayData.Result)
        End Sub
    End Module
    ' The example displays output like the following:
    '   N=100, Total = 110,081,653,682, Mean = 1,100,816,536.82
End Namespace

Die ContinueWhenAll-Methode und die ContinueWhenAny-Methode ermöglichen es Ihnen, die Ausführung von mehreren Aufgaben fortzusetzen.

Weitere Informationen finden Sie unter Verketten von Aufgaben mithilfe von Fortsetzungsaufgaben.

Erstellen von getrennten untergeordneten Aufgaben

Wenn durch Benutzercode, der in einer Aufgabe ausgeführt wird, eine neue Aufgabe ohne Angabe der AttachedToParent-Option erstellt, ist die neue Aufgabe in keiner besonderen Weise mit der übergeordneten Aufgabe synchronisiert. Dieser Typ einer nicht synchronisierten Aufgabe wird als getrennte geschachtelte Aufgabe oder getrennte untergeordnete Aufgabe bezeichnet. Im folgenden Beispiel wird eine Aufgabe dargestellt, die eine getrennte untergeordnete Aufgabe erstellt:

var outer = Task.Factory.StartNew(() =>
{
    Console.WriteLine("Outer task beginning.");

    var child = Task.Factory.StartNew(() =>
    {
        Thread.SpinWait(5000000);
        Console.WriteLine("Detached task completed.");
    });
});

outer.Wait();
Console.WriteLine("Outer task completed.");
// The example displays the following output:
//    Outer task beginning.
//    Outer task completed.
//    Detached task completed.
Dim outer = Task.Factory.StartNew(Sub()
                                      Console.WriteLine("Outer task beginning.")
                                      Dim child = Task.Factory.StartNew(Sub()
                                                                            Thread.SpinWait(5000000)
                                                                            Console.WriteLine("Detached task completed.")
                                                                        End Sub)
                                  End Sub)
outer.Wait()
Console.WriteLine("Outer task completed.")
' The example displays the following output:
'     Outer task beginning.
'     Outer task completed.
'    Detached child completed.

Hinweis

Beachten Sie, dass die übergeordnete Aufgabe nicht auf den Abschluss der getrennten untergeordneten Aufgabe wartet.

Erstellen von untergeordneten Aufgaben

Wenn durch Benutzercode, der in einer Aufgabe ausgeführt wird, eine Aufgabe mit der AttachedToParent-Option erstellt wird, wird die neue Aufgabe als angefügte untergeordnete Aufgabe der übergeordneten Aufgabe bezeichnet. Mithilfe der AttachedToParent-Option können Sie eine strukturierte Aufgabenparallelität angeben, da die übergeordnete Aufgabe implizit auf den Abschluss aller angefügten untergeordneten Aufgaben wartet. Im folgenden Beispiel wird eine übergeordnete Aufgabe dargestellt, die 10 angefügte untergeordnete Aufgaben erstellt. Im Beispiel wird die Task.Wait-Methode aufgerufen, um auf den Abschluss der übergeordneten Aufgabe zu warten. Es muss aber nicht explizit auf den Abschluss der angefügten untergeordneten Aufgaben gewartet werden.

using System;
using System.Threading;
using System.Threading.Tasks;

public class Child
{
   public static void Main()
   {
      var parent = Task.Factory.StartNew(() => {
                      Console.WriteLine("Parent task beginning.");
                      for (int ctr = 0; ctr < 10; ctr++) {
                         int taskNo = ctr;
                         Task.Factory.StartNew((x) => {
                                                  Thread.SpinWait(5000000);
                                                  Console.WriteLine("Attached child #{0} completed.",
                                                                    x);
                                               },
                                               taskNo, TaskCreationOptions.AttachedToParent);
                      }
                   });

      parent.Wait();
      Console.WriteLine("Parent task completed.");
   }
}
// The example displays output like the following:
//       Parent task beginning.
//       Attached child #9 completed.
//       Attached child #0 completed.
//       Attached child #8 completed.
//       Attached child #1 completed.
//       Attached child #7 completed.
//       Attached child #2 completed.
//       Attached child #6 completed.
//       Attached child #3 completed.
//       Attached child #5 completed.
//       Attached child #4 completed.
//       Parent task completed.
Imports System.Threading

Namespace Child
    Module Example
        Public Sub Main()
            Dim parent = Task.Factory.StartNew(Sub()
                                                   Console.WriteLine("Parent task beginning.")
                                                   For ctr As Integer = 0 To 9
                                                       Dim taskNo As Integer = ctr
                                                       Task.Factory.StartNew(Sub(x)
                                                                                 Thread.SpinWait(5000000)
                                                                                 Console.WriteLine("Attached child #{0} completed.",
                                                                                                 x)
                                                                             End Sub,
                                                       taskNo, TaskCreationOptions.AttachedToParent)
                                                   Next
                                               End Sub)
            parent.Wait()
            Console.WriteLine("Parent task completed.")
        End Sub
    End Module
    ' The example displays output like the following:
    '       Parent task beginning.
    '       Attached child #9 completed.
    '       Attached child #0 completed.
    '       Attached child #8 completed.
    '       Attached child #1 completed.
    '       Attached child #7 completed.
    '       Attached child #2 completed.
    '       Attached child #6 completed.
    '       Attached child #3 completed.
    '       Attached child #5 completed.
    '       Attached child #4 completed.
    '       Parent task completed.
End Namespace

Durch das Angeben der TaskCreationOptions.DenyChildAttach-Option für eine übergeordnete Aufgabe kann das Anfügen anderer Aufgaben an die übergeordnete Aufgabe verhindert werden. Weitere Informationen finden Sie unter Angefügte und getrennte untergeordnete Aufgaben.

Warten auf die Beendigung von Aufgaben

Der System.Threading.Tasks.Task-Typ und der System.Threading.Tasks.Task<TResult>-Typ bieten mehrere Überladungen der Task.Wait-Methode, die das Warten auf den Abschluss einer Aufgabe ermöglichen. Außerdem können Sie mittels Überladungen der statischen Task.WaitAll-Methode und der Task.WaitAny-Methode auf den Abschluss einer bestimmten oder aller Aufgaben in einem Array warten.

In der Regel wird aus einem der folgenden Gründe auf den Abschluss einer Aufgabe gewartet:

  • Der Hauptthread hängt von dem Endergebnis ab, das von einer Aufgabe berechnet wird.

  • Sie müssen Ausnahmen behandeln, die möglicherweise von der Aufgabe ausgelöst werden.

  • Die Anwendung wird möglicherweise beendet, ehe die Ausführung aller Aufgaben abgeschlossen ist. Beispielsweise werden Konsolenanwendungen beendet, nachdem der gesamte synchrone Code in Main (dem Einstiegspunkt der Anwendung) ausgeführt wurde.

Im folgenden Beispiel wird das grundlegende Muster dargestellt, das keine Ausnahmebehandlung beinhaltet:

Task[] tasks = new Task[3]
{
    Task.Factory.StartNew(() => MethodA()),
    Task.Factory.StartNew(() => MethodB()),
    Task.Factory.StartNew(() => MethodC())
};

//Block until all tasks complete.
Task.WaitAll(tasks);

// Continue on this thread...
Dim tasks() =
{
    Task.Factory.StartNew(Sub() MethodA()),
    Task.Factory.StartNew(Sub() MethodB()),
    Task.Factory.StartNew(Sub() MethodC())
}

' Block until all tasks complete.
Task.WaitAll(tasks)

' Continue on this thread...

Ein Beispiel, in dem die Behandlung von Ausnahmen veranschaulicht wird, finden Sie unter Ausnahmebehandlung.

Bei einigen Überladungen können Sie einen Timeout angeben, während andere ein zusätzliches CancellationToken als Eingabeparameter nutzen, sodass der Wartevorgang selbst entweder programmgesteuert oder als Reaktion auf eine Benutzereingabe abgebrochen werden kann.

Beim Warten auf eine Aufgabe wird implizit auf alle untergeordneten Elemente dieser Aufgabe gewartet, die mit der TaskCreationOptions.AttachedToParent-Option erstellt wurden. Task.Wait wird sofort zurückgegeben, wenn die Aufgabe bereits abgeschlossen wurde. Alle von einer Aufgabe ausgelösten Ausnahmen werden von einer Task.Wait-Methode ausgelöst, auch wenn die Task.Wait-Methode nach Abschluss der Aufgabe aufgerufen wurde.

Verfassen von Aufgaben

Die Klassen Task und Task<TResult> bieten verschiedene Methoden, mit denen Sie mehrere Aufgaben erstellen können. Diese Methoden implementieren allgemeine Muster und nutzen die asynchronen Sprachfeatures, die von C#, Visual Basic und F# bereitgestellt werden, besser. In diesem Abschnitt werden die Methoden WhenAll, WhenAny, Delay und FromResult beschrieben.

Task.WhenAll

Die Task.WhenAll-Methode wartet asynchron auf den Abschluss mehrerer Task-Objekte oder Task<TResult>-Objekte. Die Methode stellt überladene Versionen zum Warten auf nicht einheitliche Sätze von Aufgaben bereit. Beispielsweise kann in einem Methodenaufruf auf den Abschluss mehrerer Task-Objekte und Task<TResult>-Objekte gewartet werden.

Task.WhenAny

Die Task.WhenAny-Methode wartet asynchron auf den Abschluss eines von mehreren Task-Objekten oder Task<TResult>-Objekten. Wie die Task.WhenAll-Methode stellt auch diese Methode überladene Versionen bereit, mit denen auf nicht einheitliche Sätze von Aufgaben gewartet werden kann. Die WhenAny-Methode ist insbesondere in folgenden Szenarien nützlich:

  • Redundante Vorgänge: Betrachten Sie einen Algorithmus oder einen Vorgang, der auf verschiedene Weise ausgeführt werden kann. Sie können die WhenAny-Methode verwenden, um den Vorgang auszuwählen, der zuerst beendet wird, und dann die verbleibenden Vorgänge abzubrechen.

  • Überlappende Vorgänge: Sie können mehrere Vorgänge starten, die alle beendet werden müssen, und die WhenAny-Methode verwenden, um Ergebnisse zu verarbeiten, wenn jeder Vorgang beendet wird. Nachdem ein Vorgang beendet wurde, können Sie eine oder mehrere Aufgaben starten.

  • Eingeschränkte Vorgänge: Sie können die WhenAny-Methode verwenden, um das vorherige Szenario zu erweitern, indem Sie die Anzahl der gleichzeitigen Vorgänge einschränken.

  • Abgelaufene Vorgänge: Sie können die WhenAny-Methode verwenden, um eine Auswahl zwischen einer oder mehreren Aufgaben und einer anderen Aufgabe zu treffen, die nach einem bestimmten Zeitpunkt abgeschlossen wird, z. B. die von der Delay-Methode zurückgegebene Aufgabe. Die Delay-Methode wird im folgenden Abschnitt beschrieben.

Task.Delay

Die Task.Delay-Methode generiert Task-Objekt, das nach dem angegebenen Zeitraum abgeschlossen wird. Mit dieser Methode können Sie Schleifen zum Abfragen von Daten, zur Angabe von Timeouts, zum Verzögern der Verarbeitung von Benutzereingaben usw. erstellen.

Task(T).FromResult

Mit der Task.FromResult-Methode, können Sie ein Task<TResult>-Objekt erstellen, das ein vorberechnetes Ergebnis enthält. Diese Methode ist nützlich, wenn Sie einen asynchronen Vorgang ausführen, der ein Task<TResult>-Objekt zurückgibt, und das Ergebnis dieses Task<TResult>-Objekts bereits berechnet wurde. Ein Beispiel, das FromResult verwendet, um die Ergebnisse asynchroner Downloadvorgänge aus einem Cache abzurufen, finden Sie unter Vorgehensweise: Erstellen von vorberechneten Aufgaben.

Behandeln von Ausnahmen in Aufgaben

Wenn eine Aufgabe eine oder mehrere Ausnahmen auslöst, werden die Ausnahmen in eine AggregateException-Ausnahme eingeschlossen. Diese Ausnahme wird an den Thread zurückgegeben, der mit der Aufgabe verknüpft wird. In der Regel ist es der Thread, der auf den Abschluss der Aufgabe wartet, oder der Thread, der auf die Result-Eigenschaft zugreift. Dieses Verhalten dient zur Durchsetzung der .NET Framework-Richtlinie, der zufolge alle Ausnahmefehler standardmäßig zu einem Beenden des Prozesses führen. Der aufrufende Code kann die Ausnahmen behandeln, indem er Folgendes in einem try/catch-Block verwendet:

Der Verbindungsthread kann Ausnahmen ebenfalls behandeln, indem er auf die Exception-Eigenschaft zugreift, bevor die Aufgabe der Garbage Collection zugeordnet wird. Durch den Zugriff auf diese Eigenschaft verhindern Sie, dass der Ausnahmefehler das Ausnahmeweitergabe-Verhalten auslöst, durch das der Prozess beim Abschluss des Objekts beendet wird.

Weitere Informationen zu Ausnahmen und Aufgaben finden Sie unter Ausnahmebehandlung.

Abbrechen von Aufgaben

Die Task-Klasse unterstützt einen kooperativen Abbruch und ist vollständig in die System.Threading.CancellationTokenSource-Klasse und die System.Threading.CancellationToken-Klasse integriert, die in .NET Framework 4 eingeführt wurden. Viele Konstruktoren in der System.Threading.Tasks.Task-Klasse verwenden ein CancellationToken-Objekt als Eingabeparameter. Viele der StartNew- und Run-Überladungen enthalten auch einen CancellationToken-Parameter.

Mit der CancellationTokenSource-Klasse können Sie das Token erstellen und die Abbruchanforderung zu einem späteren Zeitpunkt ausgeben. Übergeben Sie das Token als Argument an Task, und verweisen Sie in dem Benutzerdelegaten, der auf eine Abbruchanforderung reagiert, auf das gleiche Token.

Weitere Informationen finden Sie unter Aufgabenabbruch und Vorgehensweise: Abbrechen einer Aufgabe und ihrer untergeordneten Elemente.

Die TaskFactory-Klasse

Die TaskFactory-Klasse stellt statische Methoden bereit, die allgemeine Muster zum Erstellen und Starten von Aufgaben und Fortsetzungsaufgaben kapseln.

Auf die standardmäßige TaskFactory kann als statische Eigenschaft in der Task-Klasse oder Task<TResult>-Klasse zugegriffen werden. Sie können eine TaskFactory auch direkt instanziieren und verschiedene Optionen angeben, einschließlich CancellationToken, TaskCreationOptions, TaskContinuationOptions oder TaskScheduler. Die beim Erstellen der Aufgabenfactory angegebenen Optionen werden auf alle Aufgaben angewendet, die von dieser erstellt werden, außer Sie erstellen Task mit der TaskCreationOptions-Enumeration. In diesem Fall werden die Optionen der Aufgabenfactory mit den Optionen der Aufgabe überschrieben.

Aufgaben ohne Delegaten

In einigen Fällen können Sie mithilfe einer Task einen asynchronen Vorgang kapseln, der nicht von Ihrem Benutzerdelegaten, sondern von einer externen Komponente ausgeführt wird. Wenn der Vorgang auf dem Begin/End-Muster des asynchronen Programmiermodells basiert, können Sie die FromAsync-Methoden verwenden. Wenn das nicht der Fall ist, können Sie den Vorgang mithilfe des TaskCompletionSource<TResult>-Objekts in eine Aufgabe einschließen und dadurch bestimmte Vorteile der Task-Programmierbarkeit erhalten. Beispielsweise Unterstützung für Ausnahmeweitergabe und Fortsetzungen. Weitere Informationen finden Sie unter TaskCompletionSource<TResult>.

Benutzerdefinierte Planer

Die meisten Anwendungs- oder Bibliotheksentwickler machen sich keine Gedanken über den für die Ausführung der Aufgabe verwendeten Prozessor, über die Synchronisierung seiner Arbeit mit anderen Aufgaben oder die Planung im System.Threading.ThreadPool. Die einzige Voraussetzung für sie ist eine möglichst effiziente Ausführung auf dem Hostcomputer. Wenn Sie eine präzisere Steuerung der Planungsdetails benötigen, können Sie in der TPL bestimmte Einstellungen im Standardaufgabenplaner konfigurieren und sogar einen benutzerdefinierten Planer angeben. Weitere Informationen finden Sie unter TaskScheduler.

Die TPL beinhaltet zahlreiche neue öffentliche Typen, die in parallelen und sequenziellen Szenarien nützlich sind. Dazu gehören mehrere threadsichere, schnelle und skalierbare Sammlungsklassen im System.Collections.Concurrent-Namespace und mehrere neue Synchronisierungstypen. Beispielsweise System.Threading.Semaphore und System.Threading.ManualResetEventSlim, die für bestimmte Arten von Workloads effizienter sind als ihre Vorgänger. Andere neue Typen in .NET Framework 4, z. B. System.Threading.Barrier und System.Threading.SpinLock, bieten Funktionalität, die in früheren Releases nicht verfügbar war. Weitere Informationen finden Sie unter Datenstrukturen für die parallele Programmierung.

Benutzerdefinierte Aufgabentypen

Es wird empfohlen, nicht von System.Threading.Tasks.Task oder System.Threading.Tasks.Task<TResult> zu erben. Stattdessen sollten Sie die AsyncState-Eigenschaft verwenden, um einem Task-Objekt oder einem Task<TResult>-Objekt zusätzliche Daten oder einen zusätzlichen Zustand zuzuordnen. Sie können auch Erweiterungsmethoden verwenden, um die Funktionen der Task-Klasse und der Task<TResult>-Klasse zu erweitern. Weitere Informationen zu Erweiterungsmethoden finden Sie unter Erweiterungsmethoden und Erweiterungsmethoden.

Wenn das Erben von Task oder Task<TResult> unumgänglich ist, können Sie Run oder die Klassen System.Threading.Tasks.TaskFactory, System.Threading.Tasks.TaskFactory<TResult> oder System.Threading.Tasks.TaskCompletionSource<TResult> nicht zum Erstellen von Instanzen des benutzerdefinierten Aufgabentyps verwenden. Sie können sie nicht verwenden, da von diesen Klassen nur Task- und Task<TResult>-Objekte erstellt werden. Außerdem können Sie nicht die von Task, Task<TResult>, TaskFactory und TaskFactory<TResult> bereitgestellten Aufgabenfortsetzungsmechanismen verwenden, um Instanzen des benutzerdefinierten Aufgabentyps zu erstellen. Sie können sie nicht verwenden, da auch von diesen Klassen nur Task- und Task<TResult>-Objekte erstellt werden.

Titel Beschreibung
Verketten von Aufgaben mithilfe von Fortsetzungsaufgaben Beschreibt die Funktionsweise von Fortsetzungen.
Angefügte und getrennte untergeordnete Aufgaben Beschreibt den Unterschied zwischen angefügten und getrennten untergeordneten Aufgaben.
Aufgabenabbruch Beschreibt die integrierte Abbruchunterstützung des Task-Objekts.
Ausnahmebehandlung Beschreibt die Behandlung von Ausnahmen in gleichzeitigen Threads.
How to: Ausführen von parallelen Vorgängen mithilfe von „Parallel.Invoke“ Beschreibt die Verwendung von Invoke.
How to: Zurückgeben eines Werts aus einer Aufgabe Beschreibt, wie in Aufgaben Werte zurückgegeben werden.
How to: Abbrechen einer Aufgabe und ihrer untergeordneten Elemente Beschreibt, wie Aufgaben abgebrochen werden.
How to: Erstellen von vorberechneten Aufgaben Beschreibt, wie mithilfe der Task.FromResult-Methode die Ergebnisse asynchroner Downloadvorgänge aus einem Cache abgerufen werden können.
How to: Durchlaufen einer binären Struktur mit parallelen Aufgaben Beschreibt, wie Aufgaben zum Traversieren einer binären Struktur verwendet werden.
How to: Entpacken einer geschachtelten Aufgabe Veranschaulicht die Verwendung der Unwrap-Erweiterungsmethode.
Datenparallelität Beschreibt, wie Sie mithilfe von For und ForEach parallele Schleifen für Daten erstellen.
Parallele Programmierung Der Knoten auf oberster Ebene für die parallele .NET Framework-Programmierung.

Siehe auch