Niet-annuleerbare asynchrone bewerkingen annuleren

Soms hebt u annulering nodig, maar de bewerking waarop u wacht, accepteert geen CancellationToken. Kies in dat geval het gedrag dat u nodig hebt:

  • De bewerking zelf annuleren.
  • Annuleer alleen uw wachttijd.
  • Annuleer zowel de bewerking als de wachttijd.

De juiste keuze is afhankelijk van wie eigenaar is van de bewerking en welke opschoongaranties u nodig hebt.

De drie annulerings betekenissen begrijpen

Als mensen zeggen: 'Deze asynchrone aanroep annuleren', betekenen ze meestal een van de drie verschillende dingen:

  • De bewerking annuleren. Geef het signaal aan om de werkzaamheden te stoppen.
  • Annuleer de wachttijd. Wacht niet meer en ga verder met uw werkstroom, zelfs als de bewerking nog steeds wordt uitgevoerd.
  • Annuleer beide. De aanvraagbewerking annuleren en ook onmiddellijk stoppen met wachten.

Behandel deze betekenissen als afzonderlijke ontwerpbeslissingen. Als u ze mengt, wordt het annuleringsgedrag moeilijk te begrijpen.

Liever tokenbewuste API's indien beschikbaar

Controleer voordat u een wrapper toevoegt of de API al annuleringstokens ondersteunt. Moderne .NET API's hebben veel meer ondersteuning voor tokens dan oudere .NET Framework-code. Veel stream-API's in .NET bieden nu bijvoorbeeld ondersteuning voor annulering en NetworkStream asynchrone bewerkingen volgen annuleringstokens.

Gebruik tokenoverbelastingen wanneer ze bestaan:

public static class StreamExamples
{
    public static async Task<int> ReadOnceAsync(
        NetworkStream stream,
        byte[] buffer,
        CancellationToken cancellationToken)
    {
        return await stream.ReadAsync(
            buffer.AsMemory(0, buffer.Length),
            cancellationToken);
    }
}
Public Module StreamExamples
    Public Async Function ReadOnceAsync(
        stream As NetworkStream,
        buffer As Byte(),
        cancellationToken As CancellationToken) As Task(Of Integer)

        Return Await stream.ReadAsync(
            buffer.AsMemory(0, buffer.Length),
            cancellationToken)
    End Function
End Module

Alleen de wachttijd annuleren met behulp van Task.WhenAny

Wanneer een bewerking geen token accepteert, kunt u uw wachttijd annuleren door de bewerking parallel aan een taak die een token gebruikt uit te voeren. Dit patroon komt vaak voor als WithCancellation-helper.

public static class TaskCancellationExtensions
{
    public static async Task<T> WithCancellation<T>(
        this Task<T> task,
        CancellationToken cancellationToken)
    {
        if (task.IsCompleted)
            return await task.ConfigureAwait(false);

        var cancellationTaskSource = new TaskCompletionSource<bool>(
            TaskCreationOptions.RunContinuationsAsynchronously);

        using var registration = cancellationToken.Register(
            static state =>
                ((TaskCompletionSource<bool>)state!).TrySetResult(true),
            cancellationTaskSource);

        Task completed = await Task.WhenAny(task, cancellationTaskSource.Task)
            .ConfigureAwait(false);

        if (completed != task)
            throw new OperationCanceledException(cancellationToken);

        return await task.ConfigureAwait(false);
    }
}
Public Module TaskCancellationExtensions
    <Extension()>
    Public Async Function WithCancellation(Of T)(
        operationTask As Task(Of T),
        cancellationToken As CancellationToken) As Task(Of T)

        If operationTask.IsCompleted Then
            Return Await operationTask
        End If

        Dim cancellationTaskSource =
            New TaskCompletionSource(Of Boolean)(TaskCreationOptions.RunContinuationsAsynchronously)

        Using registration = cancellationToken.Register(
            Sub(state)
                DirectCast(state, TaskCompletionSource(Of Boolean)).TrySetResult(True)
            End Sub,
            cancellationTaskSource)

            Dim completed = Await Task.WhenAny(operationTask, cancellationTaskSource.Task)

            If completed IsNot operationTask Then
                Throw New OperationCanceledException(cancellationToken)
            End If
        End Using

        Return Await operationTask
    End Function
End Module

Dit patroon gebruikt WhenAny om te retourneren zodra een van beide taken is voltooid.

Gebruik deze benadering alleen wanneer het veilig is voor de oorspronkelijke bewerking om op de achtergrond door te gaan.

Beide bewerkingen annuleren en wachten wanneer u de eigenaar van de bewerking bent

Als u de eigenaar van de bewerking bent en een token accepteert, geeft u het token door en gebruikt u nog steeds een annuleerbare wachttijd wanneer dat nodig is:

public static class CancelBothDemo
{
    public static async Task<string> FetchDataAsync(CancellationToken cancellationToken)
    {
        await Task.Delay(500, cancellationToken);
        return "payload";
    }

    public static async Task RunAsync()
    {
        using var cts = new CancellationTokenSource();
        cts.CancelAfter(100);

        try
        {
            string payload = await FetchDataAsync(cts.Token)
                .WithCancellation(cts.Token);
            Console.WriteLine(payload);
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Canceled operation and wait.");
        }
    }
}
Public Module CancelBothDemo
    Public Async Function FetchDataAsync(cancellationToken As CancellationToken) As Task(Of String)
        Await Task.Delay(500, cancellationToken)
        Return "payload"
    End Function

    Public Async Function RunAsync() As Task
        Using cts = New CancellationTokenSource()
            cts.CancelAfter(100)

            Try
                Dim payload = Await FetchDataAsync(cts.Token).WithCancellation(cts.Token)
                Console.WriteLine(payload)
            Catch ex As OperationCanceledException
                Console.WriteLine("Canceled operation and wait.")
            End Try
        End Using
    End Function
End Module

Deze combinatie geeft responsieve annulering voor bellers en coƶperatief afsluiten voor het onderliggende werk.

Afgeslagen bewerkingen veilig afhandelen

Als u alleen de wachttijd annuleert, kan de oorspronkelijke taak later een fout opleveren. Behoud een referentie zodat u de voltooiing kunt observeren en uitzonderingen kunt loggen. Anders kunt u fouten missen en het oplossen van problemen moeilijker maken.

public static class ObserveLateFaultDemo
{
    private static async Task<int> FaultLaterAsync()
    {
        await Task.Delay(250);
        throw new InvalidOperationException("Background operation failed.");
    }

    public static async Task RunAsync()
    {
        Task<int> operation = FaultLaterAsync();

        using var cts = new CancellationTokenSource(50);

        try
        {
            await operation.WithCancellation(cts.Token);
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Stopped waiting; operation still running.");
        }

        _ = operation.ContinueWith(
            t => Console.WriteLine($"Observed late fault: {t.Exception!.InnerException!.Message}"),
            TaskContinuationOptions.OnlyOnFaulted);

        await Task.Delay(300);
    }
}
Public Module ObserveLateFaultDemo
    Private Async Function FaultLaterAsync() As Task(Of Integer)
        Await Task.Delay(250)
        Throw New InvalidOperationException("Background operation failed.")
    End Function

    Public Async Function RunAsync() As Task
        Dim operation As Task(Of Integer) = FaultLaterAsync()

        Using cts = New CancellationTokenSource(50)
            Try
                Await operation.WithCancellation(cts.Token)
            Catch ex As OperationCanceledException
                Console.WriteLine("Stopped waiting; operation still running.")
            End Try
        End Using

        Dim observed = operation.ContinueWith(
            Sub(t)
                Console.WriteLine($"Observed late fault: {t.Exception.InnerException.Message}")
            End Sub,
            TaskContinuationOptions.OnlyOnFaulted)

        Await observed
    End Function
End Module

Zie ook