Completare le attività

Quando si espone un'attività da TaskCompletionSource<TResult>, si è proprietari della durata dell'attività. Completare l'attività in ogni percorso. Se un percorso salta il completamento, i chiamanti aspettano all'infinito.

Completare ogni percorso di codice

Completare sempre l'attività sia nei percorsi di successo che in quelli di fallimento. Usare un blocco catch per la logica di pulizia quando l'attività ha esito negativo. Utilizzare un blocco finally per la logica di pulizia che deve sempre essere eseguita. Il blocco di codice seguente mostra l'aggiunta della pulizia per un percorso di errore:

public sealed class MissingSetExceptionFix
{
    public Task<string> StartAsync(bool fail)
    {
        var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);

        try
        {
            if (fail)
            {
                throw new InvalidOperationException("Simulated failure");
            }

            tcs.TrySetResult("success");
        }
        catch (Exception ex)
        {
            tcs.TrySetException(ex);
        }

        return tcs.Task;
    }
}
Public NotInheritable Class MissingSetExceptionFix
    Public Function StartAsync(fail As Boolean) As Task(Of String)
        Dim tcs = New TaskCompletionSource(Of String)(TaskCreationOptions.RunContinuationsAsynchronously)

        Try
            If fail Then
                Throw New InvalidOperationException("Simulated failure")
            End If

            tcs.TrySetResult("success")
        Catch ex As Exception
            tcs.TrySetException(ex)
        End Try

        Return tcs.Task
    End Function
End Class

Il codice seguente intercetta un'eccezione, lo registra e dimentica di chiamare SetException o TrySetException. Questo bug viene visualizzato spesso e fa sì che i chiamanti attendino per sempre. Per altri dettagli sulla gestione delle eccezioni con le attività, vedere Gestione delle eccezioni delle attività.

// ⚠️ DON'T copy this snippet. It demonstrates a problem that causes hangs.
public sealed class MissingSetExceptionBug
{
    public Task<string> StartAsync(bool fail)
    {
        var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);

        try
        {
            if (fail)
            {
                throw new InvalidOperationException("Simulated failure");
            }

            tcs.SetResult("success");
        }
        catch (Exception)
        {
            // BUG: forgot SetException or TrySetException.
        }

        return tcs.Task;
    }
}
' ⚠️ DON'T copy this snippet. It demonstrates a problem that causes hangs.
Public NotInheritable Class MissingSetExceptionBug
    Public Function StartAsync(fail As Boolean) As Task(Of String)
        Dim tcs = New TaskCompletionSource(Of String)(TaskCreationOptions.RunContinuationsAsynchronously)

        Try
            If fail Then
                Throw New InvalidOperationException("Simulated failure")
            End If

            tcs.SetResult("success")
        Catch ex As Exception
            ' BUG: forgot SetException or TrySetException.
        End Try

        Return tcs.Task
    End Function
End Class

Preferire TrySet* nelle competizioni di completamento

I percorsi simultanei spesso gareggiano per completare lo stesso TaskCompletionSource. SetResult, SetExceptione SetCanceled generano un'eccezione se l'attività è già stata completata. Nel codice soggetto a race usare TrySetResult, TrySetExceptione TrySetCanceled. Per altri modelli da evitare in scenari concorrenti, vedere Common async/await bugs.

public static class TrySetRaceExample
{
    public static void ShowRaceSafeCompletion()
    {
        var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);

        bool first = tcs.TrySetResult(42);
        bool second = tcs.TrySetException(new TimeoutException("Too late"));

        Console.WriteLine($"First completion won: {first}");
        Console.WriteLine($"Second completion accepted: {second}");
        Console.WriteLine($"Result: {tcs.Task.Result}");
    }
}
Public Module TrySetRaceExample
    Public Sub ShowRaceSafeCompletion()
        Dim tcs = New TaskCompletionSource(Of Integer)(TaskCreationOptions.RunContinuationsAsynchronously)

        Dim first As Boolean = tcs.TrySetResult(42)
        Dim second As Boolean = tcs.TrySetException(New TimeoutException("Too late"))

        Console.WriteLine($"First completion won: {first}")
        Console.WriteLine($"Second completion accepted: {second}")
        Console.WriteLine($"Result: {tcs.Task.Result}")
    End Sub
End Module

Non eliminare i riferimenti durante la reimpostazione

Un bug comune si verifica nelle primitive asincrone reimpostabili. Correggere il percorso di reset scambiando i riferimenti in modo atomico e completando l'attività precedente (ad esempio, con cancellazione):

public sealed class ResetFix
{
    private TaskCompletionSource<bool> _signal = NewSignal();

    public Task WaitAsync() => _signal.Task;

    public void Reset()
    {
        TaskCompletionSource<bool> previous = Interlocked.Exchange(ref _signal, NewSignal());
        previous.TrySetCanceled();
    }

    public void Pulse()
    {
        _signal.TrySetResult(true);
    }

    private static TaskCompletionSource<bool> NewSignal() =>
        new(TaskCreationOptions.RunContinuationsAsynchronously);
}
Public NotInheritable Class ResetFix
    Private _signal As TaskCompletionSource(Of Boolean) = NewSignal()

    Public Function WaitAsync() As Task
        Return _signal.Task
    End Function

    Public Sub Reset()
        Dim previous As TaskCompletionSource(Of Boolean) = Interlocked.Exchange(_signal, NewSignal())
        previous.TrySetCanceled()
    End Sub

    Public Sub Pulse()
        _signal.TrySetResult(True)
    End Sub

    Private Shared Function NewSignal() As TaskCompletionSource(Of Boolean)
        Return New TaskCompletionSource(Of Boolean)(TaskCreationOptions.RunContinuationsAsynchronously)
    End Function
End Class

Non eseguire questa operazione: Se si sostituisce un'istanza TaskCompletionSource prima di completare quella precedente, i camerieri che contengono l'attività precedente potrebbero non essere mai completati.

// ⚠️ DON'T copy this snippet. It demonstrates a problem where old waiters never complete.
public sealed class ResetBug
{
    private TaskCompletionSource<bool> _signal = NewSignal();

    public Task WaitAsync() => _signal.Task;

    public void Reset()
    {
        // BUG: waiters on the old task might never complete.
        _signal = NewSignal();
    }

    public void Pulse()
    {
        _signal.TrySetResult(true);
    }

    private static TaskCompletionSource<bool> NewSignal() =>
        new(TaskCreationOptions.RunContinuationsAsynchronously);
}
' ⚠️ DON'T copy this snippet. It demonstrates a problem where old waiters never complete.
Public NotInheritable Class ResetBug
    Private _signal As TaskCompletionSource(Of Boolean) = NewSignal()

    Public Function WaitAsync() As Task
        Return _signal.Task
    End Function

    Public Sub Reset()
        ' BUG: waiters on the old task might never complete.
        _signal = NewSignal()
    End Sub

    Public Sub Pulse()
        _signal.TrySetResult(True)
    End Sub

    Private Shared Function NewSignal() As TaskCompletionSource(Of Boolean)
        Return New TaskCompletionSource(Of Boolean)(TaskCreationOptions.RunContinuationsAsynchronously)
    End Function
End Class

Checklist

  • Completare tutte le attività esposte TaskCompletionSource in percorsi di esito positivo, negativo e annullamento.
  • Utilizzare le API TrySet* nei percorsi che potrebbero riscontrare condizioni di race.
  • Durante la reimpostazione, completare o annullare l'attività precedente prima di eliminare il relativo riferimento.
  • Aggiungere test basati su timeout in modo che gli stalli vengano interrotti rapidamente nell'integrazione continua.

Vedere anche