Avbryta icke-avbrytbara asynkrona operationer

Ibland behöver du avbrytning, men operationen som du väntar på accepterar inte en CancellationToken. I så fall väljer du det beteende du behöver:

  • Avbryt själva åtgärden.
  • Avbryt bara din väntan.
  • Avbryt både åtgärden och din väntan.

Rätt val beror på vem som äger åtgärden och vilka rensningsgarantier du behöver.

Förstå de tre betydelserna för annullering

När folk säger "avbryt det här asynkrona samtalet" betyder de vanligtvis en av tre olika saker:

  • Avbryt åtgärden. Signalera att åtgärden stoppar arbetet.
  • Avbryt väntan. Sluta vänta och fortsätt med ditt arbetsflöde, även om processen fortfarande körs.
  • Avbryt båda. Avbryt begäran och sluta vänta omedelbart.

Behandla dessa betydelser som separata designbeslut. Om du blandar dem blir avbokningsbeteendet svårt att resonera kring.

Föredrar tokenmedvetna API:er när de är tillgängliga

Innan du lägger till en omslutning kontrollerar du om API:et redan stöder annulleringstoken. Moderna .NET API:er har mycket bredare tokenstöd än äldre .NET Framework-kod. Till exempel stöder många stream-API:er i .NET nu avbrytning, och NetworkStream asynkrona åtgärder respekterar avbrytningstoken.

Använd tokenöverlagringar när de finns:

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

Avbryt bara väntan med hjälp av Task.WhenAny

När en åtgärd inte accepterar en token avbryter du din väntan genom att köra åtgärden mot en tokenstödd uppgift. Det här mönstret visas ofta som ett WithCancellation hjälpverktyg.

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

Det här mönstret används WhenAny för att returnera så snart någon av uppgifterna har slutförts.

Använd endast den här metoden när det är säkert för den ursprungliga åtgärden att fortsätta i bakgrunden.

Avbryt både åtgärden och vänta när du äger åtgärden

Om du äger operationen och den accepterar en token, skicka token och använd fortfarande en avbrytbar väntan när det behövs.

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

Den här kombinationen ger responsiv annullering för uppringare och kooperativ avstängning för det underliggande arbetet.

Hantera övergivna åtgärder på ett säkert sätt

Om du bara avbryter väntan kan den ursprungliga uppgiften senare misslyckas. Behåll en referens så att du kan observera slutförande- och loggfel. Annars kan du missa fel och göra felsökningen svårare.

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

Se även