Task.Factory.FromAsync с CancellationTokenSource

У меня есть следующая строка кода, используемая для асинхронного чтения из NetworkStream:

int bytesRead = await Task<int>.Factory.FromAsync(this.stream.BeginRead, this.stream.EndRead, buffer, 0, buffer.Length, null);

Я бы хотел, чтобы он поддерживал отмену. Я вижу, что я могу отменить задачи с помощью CancellationTokenSource, однако я не вижу никакого способа передать его TaskFactory. FromAsync().

Возможно ли сделать отмену поддержки функции FromAsync()?

Изменить: я хочу отменить задание, которое уже запущено.

Ответ 1

Gigi, к сожалению, семантическая природа FromAsync указывает, что вы только адаптируете асинхронный процесс к API TPL (TPL = Microsoft Task Parallel Library)

В сущности, TPL ReadAsync управляет самим поведением async, в то время как FromAsync только обертывает поведение (но не контролирует его).

Теперь, поскольку аннулирование является специфической конструкцией TPL, и поскольку FromAsync не имеет контроля над внутренними работами вызываемого асинхронного метода, то нет гарантированного способа чисто отменить задачу и убедиться, что все ресурсы закрыты правильно (что почему это было опущено.Если вам интересно, просто декомпилируйте метод;))

В этих ситуациях имеет смысл обернуть фактический асинхронный вызов самостоятельно в обычной задаче и обнаружить исключение OperationCancelled, которое даст вам возможность закрыть ваш поток, выполнив соответствующие вызовы.

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

Ответ 2

Нет, не существует общего способа отмены такой задачи. Отмена зависит от API.

  • Например, WebClient имеет метод Cancel.
  • A Socket или FileStream должно быть Close 'd для отмены непогашенного вызова.
  • Клиенты Web-сервиса имеют разные способы отмены вызовов.
  • ...

Это связано с тем, что исполнитель операции ввода-вывода должен поддерживать отмену.

Может показаться соблазнительным использовать NetworkStream.ReadAsync и передать токен отмены, но это Stream.ReadAsync. Последний просто выбрасывает токен. В принципе не поддерживается.

Stream.ReadAsync - это только метод базового класса. Он ничего не делает сам по себе. Конкретные операции ввода-вывода выдаются только производными классами. Они должны поддерживать отмену изначально. Поток не может ничего сделать, чтобы заставить их. Бывает, что NetworkStream не поддерживает отмену.

Я понимаю, что вы хотите отменить операцию и оставить сокет открытым. Но это невозможно. (Субъективная заметка: Это действительно печальное положение вещей. Особенно учитывая, что Windows поддерживает отмену ввода-вывода на уровне Win32. )

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

"Отмена путем игнорирования" эффективно сделать позицию потока undefined. Поток становится непригодным. Это не исключает необходимости открытия нового потока. Вам все равно придется избавиться от старого потока (в большинстве случаев) и снова открыть его. Кроме того, вы вводите concurrency.

Ответ 3

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

Однако вы можете ввести отмену для Task, которая завершает асинхронную операцию. Это не отменяет основную операцию - ваш NetworkStream все равно продолжит считывать все запрошенные байты из сокета, но это позволит вашему приложению реагировать так, как будто операция была отменена, сразу же выбросив OperationCanceledException из вашего await (и выполнение любых зарегистрированных задач). Результат основной операции после ее завершения будет проигнорирован.

Это вспомогательный метод расширения:

public static class TaskExtensions
{
    public async static Task<TResult> HandleCancellation<TResult>(
        this Task<TResult> asyncTask,
        CancellationToken cancellationToken)
    {     
        // Create another task that completes as soon as cancellation is requested.
        // http://stackoverflow.com/a/18672893/1149773
        var tcs = new TaskCompletionSource<TResult>();
        cancellationToken.Register(() =>
            tcs.TrySetCanceled(), useSynchronizationContext: false);
        var cancellationTask = tcs.Task;

        // Create a task that completes when either the async operation completes,
        // or cancellation is requested.
        var readyTask = await Task.WhenAny(asyncTask, cancellationTask);

        // In case of cancellation, register a continuation to observe any unhandled 
        // exceptions from the asynchronous operation (once it completes).
        // In .NET 4.0, unobserved task exceptions would terminate the process.
        if (readyTask == cancellationTask)
            asyncTask.ContinueWith(_ => asyncTask.Exception, 
                TaskContinuationOptions.OnlyOnFaulted | 
                TaskContinuationOptions.ExecuteSynchronously);

        return await readyTask;
    }
}

И это пример, который использует метод расширения для обработки операции как отмененной после 300 мс:

CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMilliseconds(300));

try
{
    int bytesRead = 
        await Task<int>.Factory.FromAsync(this.stream.BeginRead, this.stream.EndRead, buffer, 0, buffer.Length, null)
                               .HandleCancellation(cts.Token);
}
catch (OperationCanceledException)
{
    // Operation took longer than 300ms, and was treated as cancelled.
}