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() поддерживала отмену?

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


person Gigi    schedule 27.07.2014    source источник
comment
Просто поясню: я хочу отменить задачу, которая, возможно, уже запущена.   -  person Gigi    schedule 27.07.2014
comment
Кажется, что нет перегрузки FromAsync, которая принимает токен отмены. Одним из возможных решений было бы добавить еще один уровень — начать свое собственное действие с FromAsync, а затем использовать другое Task, которое поддерживает отмену извне для чтения потока внутри пользовательского действия.   -  person keenthinker    schedule 27.07.2014
comment
У вас есть веская причина не использовать NetworkStream.ReadAsync, которые поддерживают CancellationToken?   -  person avo    schedule 27.07.2014
comment
@avo NetworkStream.ReadAsync — это Stream.ReadAsync. Последний просто выбрасывает токен. Принципиально не поддерживается.   -  person usr    schedule 27.07.2014
comment
@usr, ты уверен, что это невозможно отменить? Я подозреваю, что OP может зарегистрировать обратный вызов с помощью CancellationToken.Register и оттуда вызвать NetworkStream.Dispose. Это должно отменить ожидающее чтение.   -  person avo    schedule 27.07.2014


Ответы (3)


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

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

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

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

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

person Stefan Z Camilleri    schedule 27.07.2014
comment
Вопрос касается отмены, и этот текст много говорит об очистке ресурсов, которая не является заботой асинхронной операции сама по себе, вы должны делать это независимо от того, конкурирует она или отменяется. Другой ответ более точен: в старом API нет четкой концепции отмены, только рецепты для конкретных случаев. - person hypersw; 25.09.2016

Как уже упоминали другие, нет чистого способа достичь того, о чем вы просите. Понятие отмены отсутствовало в Модели асинхронного программирования< /а>; таким образом, его нельзя было модернизировать с помощью преобразователей 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.
}
person Douglas    schedule 27.07.2014
comment
Это работает, но имейте в виду, что ввод-вывод по-прежнему будет вызывать побочные эффекты и фактически сделает позицию потока неопределенной. Поток становится непригодным. На самом деле это не избавляет от необходимости открывать новый поток. - person usr; 27.07.2014
comment
Хотя это очень хороший метод, я не уверен, что это вопрос пользователя. Прежде всего, привязка жестко заданного тайм-аута к операции ввода-вывода не является правильным подходом, поскольку производительность ввода-вывода зависит от аппаратного обеспечения. Метод расширения также хорош, но он не обрабатывает ресурсы ввода-вывода правильно, как правильно указывает @usr. - person Stefan Z Camilleri; 27.07.2014
comment
Тайм-аут был просто примером для демонстрации использования метода расширения. @usr правильный, хотя ограничение возникает из-за неспособности класса Stream обрабатывать одновременные чтения. Метод расширения будет работать без проблем с классами, которые могут выполнять асинхронные операции одновременно. - person Douglas; 27.07.2014

Нет, универсального способа отменить такую ​​задачу не существует. Отмена зависит от API.

  • Например, у WebClient есть метод Cancel.
  • Socket или FileStream необходимо ввести Close, чтобы отменить невыполненный вызов.
  • У клиентов веб-сервисов есть даже разные способы прерывания вызовов.
  • ...

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

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

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

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

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

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

person usr    schedule 27.07.2014