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

Каков правильный способ отмены асинхронной операции, которая не принимает CancellationToken?

Каков правильный способ отмены следующего?

var tcpListener = new TcpListener(connection);
tcpListener.Start();
var client = await tcpListener.AcceptTcpClientAsync();

Просто вызов tcpListener.Stop(), похоже, приводит к ObjectDisposedException, а метод AcceptTcpClientAsync не принимает структуру CancellationToken.

Неужели я совершенно пропущу что-то очевидное?

4b9b3361

Ответ 1

Предполагая, что вы не хотите вызывать метод Stop в классе TcpListener, здесь нет идеального решения.

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

public static async Task<T> WithWaitCancellation<T>( 
    this Task<T> task, CancellationToken cancellationToken) 
{
    // The tasck completion source. 
    var tcs = new TaskCompletionSource<bool>(); 

    // Register with the cancellation token.
    using(cancellationToken.Register( 
                s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs)) 
        // If the task waited on is the cancellation token...
        if (task != await Task.WhenAny(task, tcs.Task)) 
            throw new OperationCanceledException(cancellationToken); 

    // Wait for one or the other to complete.
    return await task; 
}

Выше приведено сообщение Stephen Toub в блоге "Как отменить отмененные асинхронные операции?" .

Здесь повторяется оговорка, это фактически не отменяет операцию, потому что не существует перегрузки метода AcceptTcpClientAsync, который принимает CancellationToken, он не может быть отменен.

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

С этой целью я переименовал метод из WithCancellation в WithWaitCancellation, чтобы указать, что вы отменяете ожидание, а не фактическое действие.

Оттуда он прост в использовании в вашем коде:

// Create the listener.
var tcpListener = new TcpListener(connection);

// Start.
tcpListener.Start();

// The CancellationToken.
var cancellationToken = ...;

// Have to wait on an OperationCanceledException
// to see if it was cancelled.
try
{
    // Wait for the client, with the ability to cancel
    // the *wait*.
    var client = await tcpListener.AcceptTcpClientAsync().
        WithWaitCancellation(cancellationToken);
}
catch (AggregateException ae)
{
    // Async exceptions are wrapped in
    // an AggregateException, so you have to
    // look here as well.
}
catch (OperationCancelledException oce)
{
    // The operation was cancelled, branch
    // code here.
}

Обратите внимание, что вам придется обернуть вызов для вашего клиента, чтобы захватить экземпляр OperationCanceledException, если ожидание будет отменено.

Я также выбрал AggregateException, поскольку исключения были обернуты при броске из асинхронных операций (в этом случае вы должны проверить себя).

Это оставляет вопрос о том, какой подход является лучшим подходом перед лицом метода, подобного методу Stop (в основном, все, что сильно срывает все, независимо от того, что происходит), что, конечно же, зависит от ваших обстоятельств.

Если вы не используете ресурс, который вы ожидаете (в данном случае, TcpListener), то, вероятно, было бы лучше использовать ресурсы для вызова метода прерывания и проглатывания любых исключений, возникающих из операций вы ждете (вам придется немного перевернуть, когда вы вызываете стоп и контролируете этот бит в других областях, которые вы ожидаете от операции). Это добавляет некоторую сложность в код, но если вы беспокоитесь об использовании ресурсов и их очистке как можно скорее, и этот выбор доступен вам, то это путь.

Если использование ресурсов не является проблемой, и вам удобнее использовать более совлокальный механизм, и вы не используете ресурс, то использование метода WithWaitCancellation прекрасное. Плюсы здесь состоят в том, что это более чистый код и легче поддерживать.

Ответ 2

В то время как ответ casperOne верен, существует более чистая потенциальная реализация для метода расширения WithCancellation (или WithWaitCancellation), который достигает одних и тех же целей:

static Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    return task.IsCompleted
        ? task
        : task.ContinueWith(
            completedTask => completedTask.GetAwaiter().GetResult(),
            cancellationToken,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
}
  • Сначала у нас есть оптимизация быстрого пути, проверяя, завершена ли задача.
  • Затем мы просто регистрируем продолжение исходной задачи и передаем параметр CancellationToken.
  • Продолжение извлекает исходный результат задачи (или исключение, если есть) синхронно, если это возможно (TaskContinuationOptions.ExecuteSynchronously), и используя поток ThreadPool, если нет (TaskScheduler.Default), наблюдая за CancellationToken для отмены.

Если исходная задача завершается до отмены CancellationToken, тогда возвращаемая задача сохраняет результат, в противном случае задача отменяется и будет вызывать TaskCancelledException при ожидании.