Как избежать SetResult после отмены задачи при использовании CancelAfter

#c# #task-parallel-library

Вопрос:

Я преобразовал EAP в TAP. Однако событие, которое обрабатывается, может вообще не запускаться, поэтому я добавляю тайм-аут. См. Пример кода

 class Request<T> {
    TaskCompletionSource<T> tcs;
    CancellationTokenSource cts;
    public int Timeout { get; }
    public bool IsCanceled => tcs.Task.IsCanceled;

    public Request(int timeout = System.Threading.Timeout.Infinite) => Timeout = timeout;

    public Task<T> StartRequestAsync() {
        cts.Token.Register(() => tcs.SetCanceled());
        cts.CancelAfter(Timeout);
        return tcs.Task;
    }

    public void OnEvent(T result) {
        tcs.SetResult(result);
    }
}
 

Обратите внимание, что CancellationTokenSource CancellationTokenRegistration оба и расположены должным образом в реализации IDisposable текущего класса. Этот StartRequestAsync метод также поточно-безопасно предотвращается от запуска более одного раза. Код упрощен для краткости.

Теперь я внешне устанавливаю результат выполнения задачи.

 var req = new Request<int>();
if(!req.IsCanceled) 
{
    req.SetResult(5);
}
 

Но я думаю, что есть расовое условие. Я предполагаю, что маркер отмены CancelAfter работает как прерывание. Если токен будет отменен ровно после того, как выполнение войдет в if блок, мы могли бы получить InvalidOperationException: An attempt was made to transition a task to a final state when it had already completed.

В документации говорится, что TaskCompletionSource потокобезопасен, так значит ли это, что TrySetResult это атомарная версия того, чего я пытаюсь достичь здесь? Это то, что я должен использовать вместо этого?

Есть ли какой-нибудь другой способ решить эту проблему?

Комментарии:

1. Да, TrySetResult является атомарным и потокобезопасным и будет делать то, что вы хотите.

2. @canton7 Спасибо

3. Тем не менее, вы, вероятно, тоже хотите, чтобы TrySetCanceled, по той же причине

Ответ №1:

Это может помочь увидеть, как SetResult SetCanceled реализуются методы и:

 public void SetResult(TResult result)
{
    if (!TrySetResult(result))
        throw new InvalidOperationException(
            Environment.GetResourceString("TaskT_TransitionToFinal_AlreadyCompleted"));
}

public void SetCanceled()
{
    if(!TrySetCanceled())
        throw new InvalidOperationException(
            Environment.GetResourceString("TaskT_TransitionToFinal_AlreadyCompleted"));
}
 

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