Como: Cancelar uma consulta PLINQ
Os exemplos a seguir mostram duas maneiras de cancelar uma consulta PLINQ. O primeiro exemplo mostra como cancelar uma consulta que consiste principalmente na travessia de dados. O segundo exemplo mostra como cancelar uma consulta que contém uma função de usuário que é computacionalmente cara.
Nota
Quando "Just My Code" está habilitado, o Visual Studio quebrará na linha que lança a exceção e exibirá uma mensagem de erro que diz "exceção não manipulada pelo código do usuário". Este erro é benigno. Você pode pressionar F5 para continuar a partir dele e ver o comportamento de manipulação de exceções demonstrado nos exemplos abaixo. Para evitar que o Visual Studio quebre no primeiro erro, basta desmarcar a caixa de seleção "Apenas meu código" em Ferramentas, Opções, Depuração, Geral.
Este exemplo destina-se a demonstrar o uso e pode não ser executado mais rápido do que a consulta LINQ to Objects sequencial equivalente. Para obter mais informações sobre speedup, consulte Understanding Speedup in PLINQ.
Exemplo 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
A estrutura PLINQ não rola um único OperationCanceledException em um System.AggregateExceptionbloco de captura, o deve ser manipulado OperationCanceledException em um bloco de captura separado. Se um ou mais delegados de usuário lançarem um OperationCanceledException(externalCT) (usando um externo System.Threading.CancellationToken), mas nenhuma outra exceção, e a consulta tiver sido definida como AsParallel().WithCancellation(externalCT)
, o PLINQ emitirá um único OperationCanceledException (externalCT) em vez de um System.AggregateException. No entanto, se um delegado de usuário lançar um OperationCanceledException, e outro delegado lançar outro tipo de exceção, ambas as exceções serão roladas em um AggregateExceptionarquivo .
As orientações gerais sobre o cancelamento são as seguintes:
Se você realizar o cancelamento user-delegate, você deve informar PLINQ sobre o externo CancellationToken e lançar um OperationCanceledException(externalCT).
Se o cancelamento ocorrer e nenhuma outra exceção for lançada, manipule um OperationCanceledException em vez de um AggregateException.
Exemplo 2
O exemplo a seguir mostra como lidar com o cancelamento quando você tem uma função computacionalmente cara no código do usuário.
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
Quando você lida com o cancelamento no código do usuário, não é necessário usar WithCancellation na definição de consulta. No entanto, recomendamos que você use WithCancellationo , porque WithCancellation não tem efeito sobre o desempenho da consulta e permite que o cancelamento seja tratado pelos operadores de consulta e seu código de usuário.
Para garantir a capacidade de resposta do sistema, recomendamos que verifique o cancelamento cerca de uma vez por milissegundo; no entanto, qualquer período até 10 milissegundos é considerado aceitável. Essa frequência não deve ter um impacto negativo no desempenho do seu código.
Quando um enumerador é descartado, por exemplo, quando o código quebra de um loop foreach (For Each no Visual Basic) que está iterando sobre os resultados da consulta, a consulta é cancelada, mas nenhuma exceção é lançada.