Vorgehensweise: Abbrechen einer PLINQ-Abfrage

In den folgenden Beispielen werden zwei Möglichkeiten zum Ändern einer PLINQ-Abfrage veranschaulicht. Das erste Beispiel zeigt, wie eine Abfrage abgebrochen wird, die größtenteils aus Datendurchlauf besteht. Im zweiten Beispiel wird gezeigt, wie eine Abfrage abgebrochen wird, die eine rechenintensive Benutzerfunktion enthält.

Hinweis

Wenn „Nur eigenen Code“ aktiviert ist, unterbricht Visual Studio die Ausführung in der Zeile, die die Ausnahme auslöst, und eine Fehlermeldung zu einer „nicht vom Benutzercode behandelten Ausnahme“ wird angezeigt. Dies ist ein gutartiger Fehler. Sie können F5 drücken, um den Vorgang fortzusetzen. In diesem Fall wird das in den nachstehenden Beispielen veranschaulichte Ausnahmebehandlungsverhalten angewendet. Um zu verhindern, dass Visual Studio beim ersten Fehler abbricht, deaktivieren Sie einfach unter Extras, Optionen, Debugging, Allgemein das Kontrollkästchen „Nur eigenen Code“.

Dieses Beispiel soll die Nutzung darstellen und wird möglicherweise nicht schneller ausgeführt als die entsprechende sequenzielle LINQ to Objects-Abfrage. Weitere Informationen finden Sie unter Grundlagen zur Beschleunigung in PLINQ.

Beispiel 1

namespace PLINQCancellation_1
{
    using System;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using static System.Console;

    class Program
    {
        static void Main()
        {
            int[] source = Enumerable.Range(1, 10000000).ToArray();
            using CancellationTokenSource cts = new();

            // Start a new asynchronous task that will cancel the
            // operation from another thread. Typically you would call
            // Cancel() in response to a button click or some other
            // user interface event.
            Task.Factory.StartNew(() =>
            {
                UserClicksTheCancelButton(cts);
            });

            int[]? results = null;
            try
            {
                results =
                    (from num in source.AsParallel().WithCancellation(cts.Token)
                     where num % 3 == 0
                     orderby num descending
                     select num).ToArray();
            }
            catch (OperationCanceledException e)
            {
                WriteLine(e.Message);
            }
            catch (AggregateException ae)
            {
                if (ae.InnerExceptions != null)
                {
                    foreach (Exception e in ae.InnerExceptions)
                    {
                        WriteLine(e.Message);
                    }
                }
            }

            foreach (var item in results ?? Array.Empty<int>())
            {
                WriteLine(item);
            }
            WriteLine();
            ReadKey();
        }

        static void UserClicksTheCancelButton(CancellationTokenSource cts)
        {
            // Wait between 150 and 500 ms, then cancel.
            // Adjust these values if necessary to make
            // cancellation fire while query is still executing.
            Random rand = new();
            Thread.Sleep(rand.Next(150, 500));
            cts.Cancel();
        }
    }
}
Class Program
    Private Shared Sub Main(ByVal args As String())
        Dim source As Integer() = Enumerable.Range(1, 10000000).ToArray()
        Dim cs As New CancellationTokenSource()

        ' Start a new asynchronous task that will cancel the 
        ' operation from another thread. Typically you would call
        ' Cancel() in response to a button click or some other
        ' user interface event.
        Task.Factory.StartNew(Sub()
                                  UserClicksTheCancelButton(cs)
                              End Sub)

        Dim results As Integer() = Nothing
        Try

            results = (From num In source.AsParallel().WithCancellation(cs.Token) _
                       Where num Mod 3 = 0 _
                       Order By num Descending _
                       Select num).ToArray()
        Catch e As OperationCanceledException

            Console.WriteLine(e.Message)
        Catch ae As AggregateException

            If ae.InnerExceptions IsNot Nothing Then
                For Each e As Exception In ae.InnerExceptions
                    Console.WriteLine(e.Message)
                Next
            End If
        Finally
            cs.Dispose()
        End Try

        If results IsNot Nothing Then
            For Each item In results
                Console.WriteLine(item)
            Next
        End If
        Console.WriteLine()

        Console.ReadKey()
    End Sub

    Private Shared Sub UserClicksTheCancelButton(ByVal cs As CancellationTokenSource)
        ' Wait between 150 and 500 ms, then cancel.
        ' Adjust these values if necessary to make
        ' cancellation fire while query is still executing.
        Dim rand As New Random()
        Thread.Sleep(rand.[Next](150, 350))
        cs.Cancel()
    End Sub
End Class

Das PLINQ-Framework setzt keine einzelne OperationCanceledException in eine System.AggregateException zurück; die OperationCanceledException muss in einem separaten Catch-Block behandelt werden. Wenn mindestens ein Benutzerdelegat eine OperationCanceledException(externalCT) (mithilfe einer externen System.Threading.CancellationToken) auslöst, aber keine weitere Ausnahme, und die Abfrage als AsParallel().WithCancellation(externalCT) definiert wurde, dann gibt PLINQ eine einzelne OperationCanceledException(externalCT) anstelle einer System.AggregateException aus. Löst jedoch ein Benutzerdelegat eine OperationCanceledException und ein anderer Delegat einen anderen Ausnahmetyp aus, werden beide Ausnahmen in eine AggregateException zurückgesetzt.

Der allgemeine Leitfaden zum Abbruch lautet wie folgt:

  1. Wenn Sie einen Benutzerdelegatenabbruch ausführen, sollten Sie PLINQ über das externe CancellationToken informieren und eine OperationCanceledException(externalCT) auslösen.

  2. Wenn ein Abbruch auftritt und keine anderen Ausnahmen ausgelöst werden, sollten Sie eine OperationCanceledException anstelle einer AggregateException behandeln.

Beispiel 2

Das folgende Beispiel zeigt, wie Sie einen Abbruch behandeln müssen, wenn der Benutzercode eine rechenintensive Funktion enthält.

namespace PLINQCancellation_2
{
    using System;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using static System.Console;

    class Program
    {
        static void Main(string[] args)
        {
            int[] source = Enumerable.Range(1, 10000000).ToArray();
            using CancellationTokenSource cts = new();

            // Start a new asynchronous task that will cancel the
            // operation from another thread. Typically you would call
            // Cancel() in response to a button click or some other
            // user interface event.
            Task.Factory.StartNew(() =>
            {
                UserClicksTheCancelButton(cts);
            });

            double[]? results = null;
            try
            {
                results =
                    (from num in source.AsParallel().WithCancellation(cts.Token)
                     where num % 3 == 0
                     select Function(num, cts.Token)).ToArray();
            }
            catch (OperationCanceledException e)
            {
                WriteLine(e.Message);
            }
            catch (AggregateException ae)
            {
                if (ae.InnerExceptions != null)
                {
                    foreach (Exception e in ae.InnerExceptions)
                        WriteLine(e.Message);
                }
            }

            foreach (var item in results ?? Array.Empty<double>())
            {
                WriteLine(item);
            }
            WriteLine();
            ReadKey();
        }

        // A toy method to simulate work.
        static double Function(int n, CancellationToken ct)
        {
            // If work is expected to take longer than 1 ms
            // then try to check cancellation status more
            // often within that work.
            for (int i = 0; i < 5; i++)
            {
                // Work hard for approx 1 millisecond.
                Thread.SpinWait(50000);

                // Check for cancellation request.
                ct.ThrowIfCancellationRequested();
            }
            // Anything will do for our purposes.
            return Math.Sqrt(n);
        }

        static void UserClicksTheCancelButton(CancellationTokenSource cts)
        {
            // Wait between 150 and 500 ms, then cancel.
            // Adjust these values if necessary to make
            // cancellation fire while query is still executing.
            Random rand = new();
            Thread.Sleep(rand.Next(150, 500));
            WriteLine("Press 'c' to cancel");
            if (ReadKey().KeyChar == 'c')
            {
                cts.Cancel();
            }
        }
    }
}
Class Program2
    Private Shared Sub Main(ByVal args As String())


        Dim source As Integer() = Enumerable.Range(1, 10000000).ToArray()
        Dim cs As New CancellationTokenSource()

        ' Start a new asynchronous task that will cancel the 
        ' operation from another thread. Typically you would call
        ' Cancel() in response to a button click or some other
        ' user interface event.
        Task.Factory.StartNew(Sub()

                                  UserClicksTheCancelButton(cs)
                              End Sub)

        Dim results As Double() = Nothing
        Try

            results = (From num In source.AsParallel().WithCancellation(cs.Token) _
                       Where num Mod 3 = 0 _
                       Select [Function](num, cs.Token)).ToArray()
        Catch e As OperationCanceledException


            Console.WriteLine(e.Message)
        Catch ae As AggregateException
            If ae.InnerExceptions IsNot Nothing Then
                For Each e As Exception In ae.InnerExceptions
                    Console.WriteLine(e.Message)
                Next
            End If
        Finally
            cs.Dispose()
        End Try

        If results IsNot Nothing Then
            For Each item In results
                Console.WriteLine(item)
            Next
        End If
        Console.WriteLine()

        Console.ReadKey()
    End Sub

    ' A toy method to simulate work.
    Private Shared Function [Function](ByVal n As Integer, ByVal ct As CancellationToken) As Double
        ' If work is expected to take longer than 1 ms
        ' then try to check cancellation status more
        ' often within that work.
        For i As Integer = 0 To 4
            ' Work hard for approx 1 millisecond.
            Thread.SpinWait(50000)

            ' Check for cancellation request.
            If ct.IsCancellationRequested Then
                Throw New OperationCanceledException(ct)
            End If
        Next
        ' Anything will do for our purposes.
        Return Math.Sqrt(n)
    End Function

    Private Shared Sub UserClicksTheCancelButton(ByVal cs As CancellationTokenSource)
        ' Wait between 150 and 500 ms, then cancel.
        ' Adjust these values if necessary to make
        ' cancellation fire while query is still executing.
        Dim rand As New Random()
        Thread.Sleep(rand.[Next](150, 350))
        Console.WriteLine("Press 'c' to cancel")
        If Console.ReadKey().KeyChar = "c"c Then
            cs.Cancel()

        End If
    End Sub
End Class

Wenn Sie den Abbruch im Benutzercode behandeln, müssen Sie WithCancellation nicht in der Abfragedefinition verwenden. Allerdings sollten Sie WithCancellation verwenden, da WithCancellation keine Auswirkung auf die Abfrageleistung hat und die Behandlung des Abbruchs durch Abfrageoperatoren und Ihren Benutzercode ermöglicht.

Um die Reaktionsfähigkeit des Systems zu gewährleisten, sollten Sie etwa einmal pro Millisekunde prüfen, ob ein Abbruch vorliegt; allerdings ist jeder Zeitraum von bis zu 10 Millisekunden akzeptabel. Diese Häufigkeit sollte sich nicht negativ auf die Leistung Ihres Codes auswirken.

Wenn ein Enumerator verworfen wird, z. B. wenn Code aus einer foreach-Schleife („For Each“ in Visual Basic) ausbricht, die eine Iteration über Abfrageergebnisse durchführt, wird die Abfrage abgebrochen, jedoch keine Ausnahme ausgelöst.

Siehe auch