Подтвердить что ты не робот

Почему мой async/ждут с утечкой памяти CancellationTokenSource?

У меня есть приложение .NET(С#), которое широко использует async/await. Я чувствую, что у меня голова вокруг async/await, но я пытаюсь использовать библиотеку (RestSharp), которая имеет более старую (или, может быть, я просто говорю другую) модель программирования, которая использует обратные вызовы для асинхронных операций.

Класс RestSharp RestClient имеет метод ExecuteAsync, который принимает параметр обратного вызова, и я хотел иметь возможность помещать оболочку вокруг того, что позволило бы мне await выполнить всю операцию. Метод ExecuteAsync выглядит примерно так:

public RestRequestAsyncHandle ExecuteAsync(IRestRequest request, Action<IRestResponse> callback);

Я думал, что все это прекрасно работает. Я использовал TaskCompletionSource, чтобы обернуть вызов ExecuteAsync в то, что я мог ждать, следующим образом:

public static async Task<T> ExecuteRequestAsync<T>(RestRequest request, CancellationToken cancellationToken) where T : new()
{
    var response = await ExecuteTaskAsync(request, cancellationToken);

    cancellationToken.ThrowIfCancellationRequested();

    return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(response.Content);
}

private static async Task<IRestResponse> ExecuteTaskAsync(RestRequest request, CancellationToken cancellationToken)
{
    var taskCompletionSource = new TaskCompletionSource<IRestResponse>();

    var asyncHandle = _restClient.ExecuteAsync(request, r => 
    {
        taskCompletionSource.SetResult(r); 
    });

    cancellationToken.Register(() => asyncHandle.Abort());

    return await taskCompletionSource.Task;
}

Это отлично работает для большинства моих приложений.

Однако у меня есть одна часть приложения, которая выполняет сотни вызовов моего ExecuteRequestAsync как часть одной операции, и эта операция показывает диалог прогресса с кнопкой отмены. Вы увидите, что в приведенном выше коде я передаю CancellationToken в ExecuteRequestAsync; для этой долговременной операции токен связан с CancellationTokenSource "принадлежностью" к диалогу, метод Cancel вызывается, если использование нажимает кнопку отмены. Пока все хорошо (кнопка отмены работает).

Моя проблема заключается в том, что во время работы приложения работает всплывающее приложение, в то время как у него заканчивается память до завершения операции.

Я запустил профайлер памяти и обнаружил, что у меня есть много объектов RestResponse, все еще находящихся в памяти, даже после сбора мусора. (Они, в свою очередь, имеют огромное количество данных, потому что я отправляю файлы с несколькими мегабайтами через провод).

Согласно профилировщику, те объекты RestResponse сохраняются в живых, потому что они ссылаются на TaskCompletionSource (через Task), который, в свою очередь, сохраняется в живых, поскольку он ссылается на CancellationTokenSource через список зарегистрированных обратных вызовов.

Из всего этого я понимаю, что регистрация обратного вызова отмены для каждого запроса означает, что весь график объектов, связанных со всеми этими запросами, будет работать до завершения всей операции. Неудивительно, что у него заканчивается память: -)

Итак, я думаю, что мой вопрос не столько "зачем он течет", сколько "как его остановить". Я не могу не зарегистрировать обратный вызов, так что я могу сделать?

4b9b3361

Ответ 1

Я не могу отменить обратный вызов

Собственно, вы можете. Возвращаемое значение Register() равно:

Экземпляр CancellationTokenRegistration, который можно использовать для отмены регистрации обратного вызова.

Чтобы действительно отменить регистрацию обратного вызова, вызовите Dispose() по возвращаемому значению.

В вашем случае вы можете сделать это следующим образом:

private static async Task<IRestResponse> ExecuteTaskAsync(
    RestRequest request, CancellationToken cancellationToken)
{
    var taskCompletionSource = new TaskCompletionSource<IRestResponse>();

    var asyncHandle = _restClient.ExecuteAsync(
        request, r => taskCompletionSource.SetResult(r));

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

Ответ 2

Вам необходимо сохранить CancellationTokenRegistration, возвращенный вызовом Register(). Устранение того, что CancellationTokenRegistration не регистрирует обратный вызов.