Невозможно отменить задачу, возвращенную TaskCompletionSource, путем введения тайм-аута в C#

У меня есть следующий метод, который ожидает события, чтобы установить результаты с помощью TaskCompletionSource.

private async Task<string?> RequestDataAsync(string? remoteEntity, CancellationToken cancellationToken)
{
    var taskCompletionSource = new TaskCompletionSource<string?>();
    try
    {
        if (remoteEntity is not null)
        {
            var handler = GetHandler();
            handler.Event.OnResultReady += result =>
            {
                taskCompletionSource.SetResult(result);
            };
            await SendRequestToAcquireDataAsync(cancellationToken);
        }
        else
        {
            taskCompletionSource.SetResult(null);
        }
    }
    catch (Exception ex)
    {
        taskCompletionSource.SetException(ex);
    }
    return await taskCompletionSource.Task;
}

Как указано в коде, мы подписываемся на событие OnResultReady и после строки подписки отправляем запрос на удаленный сервер для получения данных. Сервер обычно достаточно быстр, чтобы предоставить результаты, но были случаи, когда сервер не реагировал достаточно быстро. Поэтому мы хотим завершить работу RequestDataAsync() через 10 секунд, чтобы предотвратить блокировку клиентского приложения. Итак, я написал следующий код-оболочку для вызова метода RequestDataAsync() с использованием токена отмены, который срабатывает через 10 секунд:

    public async Task<string?> RequestDataWithTimeoutAsync(string? remoteEntity, CancellationToken cancellationToken, int requestLifetimeInMilliseconds = 10000)
    {
        var lifeTimeCancellationTokenSource = new CancellationTokenSource();
        using var linkedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, lifeTimeCancellationTokenSource.Token);
        lifeTimeCancellationTokenSource.CancelAfter(requestLifetimeInMilliseconds);
        string? result = null;
        try
        {
            result = await RequestDataAsync(remoteEntity, linkedCancellationToken.Token);
        }
        catch (OperationCanceledException oex) when (oex.CancellationToken == linkedCancellationToken.Token)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                throw new OperationCanceledException(oex.Message, oex, cancellationToken);
            }
            else if (lifeTimeCancellationTokenSource.IsCancellationRequested)
            {
                throw new TimeoutException();
            }
        }
        return result;
    }

SendRequestToAcquireDataAsync() отправляет веб-запрос в веб-API, внутренний код которого создает данные асинхронно и делает их доступными через обработчик событий обработчика. Представьте себе это как надежную лазурную функцию.

Проблема, с которой я столкнулся, заключается в том, что метод-оболочка не завершает первый метод через 10 секунд, и первый метод зависает на неопределенный срок. Каково решение этой проблемы?

Что на самом деле делает RequestDataAsync: вы полностью используете токен?

Charlieface 26.08.2024 13:43

@Charlieface Общий код не требует пояснений.

Arash 26.08.2024 14:06

@Arash Charlieface сделал правильный запрос, и я поддерживаю его. Правильно ли RequestDataAsync использует CancellationToken? Это наиболее вероятный виновник

canton7 26.08.2024 14:15

Также не нужен lifeTimeCancellationTokenSource. Сделайте using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellation‌​Token); linkedCancellationTokenSource.CancelAfter(...);

canton7 26.08.2024 14:16

@canton7 canton7 это не решает проблему.

Arash 26.08.2024 14:19

Нет, это не так. Вот почему это комментарий.

canton7 26.08.2024 14:20

Чтобы понять проблему, нам нужно увидеть RequestDataAsync.

canton7 26.08.2024 14:20

@canton7 Код RequestDataAsync уже указан в вопросе как первый метод, верно? Ключевым моментом является то, что он ожидает возникновения события handler.Event.OnResultReady. Это событие вызывается другим фрагментом кода, который получает событие от удаленного объекта, например триггер функции. Эта конкретная часть не имеет значения, поскольку речь идет о событиях. Но могут быть случаи, когда событие не возникает и, следовательно, обработчик событий не запускается.

Arash 26.08.2024 14:25

Моя вина. Нам нужно посмотреть SendRequestToAcquireDataAsync. Я не могу сказать, правильно ли оно прерывается, когда его CancellationToken отменяют, используя только силу мозга.

canton7 26.08.2024 14:27

Если SendRequestToAcquireDataAsync завершится довольно быстро, тогда да, вам нужно будет зарегистрировать действие в CancellationToken`, которое отменит ваш TCS. Вероятно, вы также захотите отказаться от подписки на это событие.

canton7 26.08.2024 14:31

@canton7 canton7 Я обновил вопрос. SendRequestToAcquireDataAsync() отправляет веб-запрос в веб-API, внутренний код которого создает данные асинхронно и делает их доступными через обработчик событий обработчика. Представьте себе это как надежную лазурную функцию. Отписка не решает проблему. Моя проблема в том, что этот метод продолжает зависать, если удаленный объект не получает событие.

Arash 26.08.2024 14:34

@ТеодорЗулиас .NET 8.0

Arash 26.08.2024 14:46

Не уверен, насколько отрицательно проголосовали за этот вопрос?! Это правильный вопрос C#, показывающий проблему, которую необходимо решить. Нежелательно делать это без уважительной причины.

Arash 26.08.2024 14:48

Посмотрите этот ответ. Существует встроенный метод WaitAsync, который делает то, что вы хотите (отмените ожидание после таймаута и позвольте задаче перейти в режим «выпустил и забыл»). Нет необходимости изобретать его заново.

Theodor Zoulias 26.08.2024 14:52

@TheodorZoulias Это тоже отличный ответ. Я попробую сегодня позже. Спасибо.

Arash 26.08.2024 15:01
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
15
67
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Если SendRequestToAcquireDataAsync корректно отменяется, когда его CancellationToken отменяется, то вы, вероятно, просто ждете, чтобы OnResultReady уволили, даже после того, как ваш CancellationToken был отменен.

Итак, вам, вероятно, потребуется зарегистрировать действие с помощью CancellationToken, чтобы отменить TaskCompletionSource в случае его отмены. Что-то вроде:

private async Task<string?> RequestDataAsync(string? remoteEntity, CancellationToken cancellationToken)
{
    var taskCompletionSource = new TaskCompletionSource<string?>();
    try
    {
        if (remoteEntity is not null)
        {
            var handler = GetHandler();
            handler.Event.OnResultReady += result =>
            {
                taskCompletionSource.TrySetResult(result);
            };
            await SendRequestToAcquireDataAsync(cancellationToken);
        }
        else
        {
            taskCompletionSource.SetResult(null);
        }
    }
    catch (Exception ex)
    {
        taskCompletionSource.SetException(ex);
    }

    using (cancellationToken.Register(() => taskCompletionSource.TrySetCanceled())
    {
        return await taskCompletionSource.Task;
    }
}

Я также изменил SetResult на TrySetResult, чтобы избежать исключения, если событие будет запущено после отмены CancellationToken.


Вероятно, вам также следует отказаться от подписки на это событие, когда все закончится:

private async Task<string?> RequestDataAsync(string? remoteEntity, CancellationToken cancellationToken)
{
    var taskCompletionSource = new TaskCompletionSource<string?>();
    Handler? handler = null;

    void ResultReadyHandler(string result)
    {
        taskCompletionSource.TrySetResult(result);
    }

    try
    {
        if (remoteEntity is not null)
        {
            handler = GetHandler();
            handler.Event.OnResultReady += ResultReadyHandler;
            await SendRequestToAcquireDataAsync(cancellationToken);
        }
        else
        {
            taskCompletionSource.SetResult(null);
        }
    }
    catch (Exception ex)
    {
        taskCompletionSource.SetException(ex);
    }

    using (cancellationToken.Register(() => taskCompletionSource.TrySetCanceled())
    {
        try
        {
            return await taskCompletionSource.Task;
        }
        finally
        {
            if (handler != null)
            {
                handler.Event.OnResultReady -= ResultReadyHandler;
            }
        }
    }
}

(Очевидно, исправьте тип Handler).


Тем не менее, вы делаете это более сложным, чем должно быть. В вашем catch вы устанавливаете исключение для TaskCompletionSource, а затем ожидаете TaskCompletionSource.Task, который просто повторно генерирует исключение. Это просто многоречивый способ просто позволить исключению всплыть, что происходит автоматически. То же самое с возвратом null:

private async Task<string?> RequestDataAsync(string? remoteEntity, CancellationToken cancellationToken)
{
    if (remoteEntity is null)
    {
        return null;
    }
  
    var taskCompletionSource = new TaskCompletionSource<string?>();

    void ResultReadyHandler(Result result)
    {
        taskCompletionSource.TrySetResult(result);
    }

    var handler = GetHandler();
    handler.Event.OnResultReady += ResultReadyHandler;

    try
    {
        await SendRequestToAcquireDataAsync(cancellationToken)
        using (cancellationToken.Register(() => taskCompletionSource.TrySetCanceled())
        {
            return await taskCompletionSource.Task;
        }
    }
    finally
    {
        handler.Event.OnResultReady -= ResultReadyHandler;
    }
}

Тем не менее, почему у SendRequestToAcquireDataAsync такой неуклюжий API? Почему он возвращает Task, который нужно дождаться, а затем запускает событие, на которое нужно подписаться?

Спасибо @canton7, сегодня попробую чуть позже и буду держать вас в курсе.

Arash 26.08.2024 14:42

потрясающее решение!! Спасибо!

Arash 27.08.2024 02:06

Другие вопросы по теме