Выполнение задач

Когда вы предоставляете задачу из TaskCompletionSource<TResult>, вы управляете жизненным циклом задачи. Выполните эту задачу на каждом пути. Если любой путь пропускает завершение, вызывающие абоненты ожидают вечно.

Завершите каждый путь кода

Всегда выполняйте задачу в путях успешного и неудачного выполнения. Используйте блок catch для кода обработки очистки при сбое задачи. Используйте блок finally для логики очистки, которая всегда должна выполняться. В следующем блоке кода показано добавление очистки для пути сбоя:

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

Следующий код перехватывает исключение, регистрирует его и забывает вызвать SetException или TrySetException. Эта ошибка часто возникает и заставляет вызывающих ждать вечно. Дополнительные сведения об обработке исключений с помощью задач см. в разделе "Обработка исключений задач".

// ⚠️ 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

Предпочтите TrySet* в соревнованиях на завершение

Параллельные пути часто соревнуются, чтобы завершить одно и то же TaskCompletionSource. SetResult, SetExceptionи SetCanceled вызов, если задача уже завершена. В коде, подверженном конкуренции, используйте TrySetResult, TrySetException и TrySetCanceled. Дополнительные шаблоны для избегания в параллельных сценариях см. в разделе распространенные ошибки при использовании async/await.

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

Не теряйте референсы во время сброса

Распространенная ошибка появляется в сбрасываемых асинхронных примитивах. Исправьте путь сброса путем атомарного переключения ссылок и завершения предыдущей задачи (например, с отменой):

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

Не делайте этого: Если вы замените TaskCompletionSource экземпляр перед завершением предыдущего, процессы, удерживающие старую задачу, могут никогда не выполнить свои задачи.

// ⚠️ 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

  • Выполните каждую предоставленную TaskCompletionSource задачу в случае успешного выполнения, сбоя или отмены.
  • Используйте TrySet* API в путях, в которых возможны состояния гонки.
  • Во время сброса завершите или отмените старую задачу перед удалением ссылки.
  • Добавьте тесты с ограничением по времени ожидания, чтобы при зависании они быстро завершались с ошибкой в системе CI.

См. также