16

我有以下代码行用于从 NetworkStream 异步读取:

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

我想让它支持取消。我看到我可以使用 CancellationTokenSource 取消任务,但是我看不到任何可以将其传递给TaskFactory.FromAsync()的方法。

是否可以使 FromAsync() 构造的任务支持取消?

编辑:我想取消一个已经在运行的任务。

4

3 回答 3

12

Gigi,不幸的是,FromAsync 的语义性质表明您只是将异步进程适应于 TPL 的 API(TPL = Microsoft 的任务并行库

本质上,TPL 的 ReadAsync 控制异步行为本身,而 FromAsync 仅包装行为(但不控制它)。

现在由于 Cancellation 是 TPL 特定的构造,并且由于 FromAsync 无法控制被调用的异步方法的内部工作,因此无法保证完全取消任务并确保正确关闭所有资源(这就是为什么它被省略了。如果你很好奇,只需反编译该方法;))

在这些情况下,将实际异步调用自己包装在正常任务中并检测 OperationCancelled 异常会更有意义,这将使您有机会通过进行适当的调用来关闭流。

简而言之,答案是否定的,但没有什么能阻止您创建一个通用的重载方法,该方法将根据流的类型选择正确的策略来干净地关闭流。

于 2014-07-27T13:44:50.933 回答
8

正如其他人已经提到的那样,没有干净的方法可以实现您的要求。异步编程模型中没有取消的概念;因此,它不能通过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.
}
于 2014-07-27T14:24:37.547 回答
6

不,没有取消此类任务的通用方法。取消是特定于 API 的。

  • 例如,WebClient有一个Cancel方法。
  • 需要ASocket或 a以取消未完成的呼叫。FileStreamClose
  • Web 服务客户端甚至有不同的中止调用方式。
  • ...

这是因为 IO 操作的实现者必须支持取消。

NetworkStream.ReadAsync使用和传递取消令牌似乎很诱人,但Stream.ReadAsync. 后者只是扔掉了令牌。基本不支持。

Stream.ReadAsync只是基类方法。它自己不做任何事情。具体的 IO 操作仅由派生类发出。那些必须原生支持取消。Stream 不能做任何事情来强迫他们。碰巧NetworkStream不支持取消。

我了解到您想取消操作并让套接字保持打开状态。但这是不可能的。(主观说明:这真是一个可悲的状态。特别是考虑到 Windows 在 Win32 级别支持可取消 IO。)

如果您仍然希望您的应用程序快速继续,尽管 IO 操作不可取消,只需忽略该任务的结果并继续。请注意,最终 IO 可能会完成,例如从套接字缓冲区中耗尽数据或导致其他副作用。

“通过忽略取消”有效地使流位置未定义。流变得不可用。这并不能真正避免打开新流的需要。您仍然必须摆脱旧流(在大多数情况下)并重新打开。此外,您正在引入并发性。

于 2014-07-27T12:35:00.040 回答