Task.Factory.FromAsync与CancellationTokenSource

18

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

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

我希望它能够支持取消操作。我看到可以使用CancellationTokenSource来取消任务,但我不知道如何将其传递给TaskFactory.FromAsync()

是否有可能使通过FromAsync()构造的任务支持取消操作?

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


只是想澄清一下:我想取消一个可能已经在运行的任务。 - Gigi
似乎FromAsync没有重载,支持取消令牌。一个可能的解决方案是添加一层 - 用FromAsync启动自己的操作,然后使用另一个从外部支持取消的Task来读取流,在自定义操作中完成。 - keenthinker
дҪ жңүжІЎжңүдёҚдҪҝз”Ёж”ҜжҢҒCancellationTokenзҡ„NetworkStream.ReadAsyncзҡ„е……еҲҶзҗҶз”ұпјҹ - avo
@avo NetworkStream.ReadAsync就是Stream.ReadAsync。而后者只是丢弃了令牌。基本上不被支持。 - usr
@usr,你确定这是不可能取消的吗?我怀疑OP可以使用CancellationToken.Register注册一个回调函数,并从那里调用NetworkStream.Dispose。这应该会取消挂起的读取操作。 - avo
3个回答

12

很抱歉,FromAsync的语义性质表明,您只是将异步过程适配到TPL的API中(TPL = Microsoft的任务并行库)。

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

由于Cancellation是一个TPL特定的构造,而FromAsync无法控制调用的异步方法的内部工作方式,因此没有一种保证能够干净地取消任务并确保所有资源被正确关闭的方法(这就是为什么它被省略的原因。如果您感兴趣,只需反编译该方法即可)。

在这些情况下,更有意义的做法是自己包装实际的异步调用,并检测OperationCancelled异常,这将为您提供机会通过进行适当的调用来关闭流。

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


1
这个问题涉及到取消操作,而这段文字则详细说明了资源清理的情况,这并不是异步操作本身所关心的,无论是竞争还是取消,你都必须进行清理。另一个回答更加直接:在旧的API中,并没有一个坚实的取消概念,只有特定情况下的解决方案。 - hypersw

9
正如其他人已经提到的,没有一种干净的方法来实现您所要求的内容。取消的概念在异步编程模型中不存在;因此,它无法通过FromAsync转换器进行改装。
但是,您可以为包装异步操作的Task引入取消。这不会取消底层操作本身——您的NetworkStream仍将继续从套接字读取所有请求的字节——但它将允许您的应用程序作出反应,就好像操作被取消一样,立即从您的await抛出一个OperationCanceledException(并执行任何已注册的任务继续)。完成后的底层操作结果将被忽略。
这是一个辅助扩展方法:
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.
        // https://dev59.com/PGMl5IYBdhLWcg3wHj5O#18672893
        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.
}

这个可以工作,但请注意IO仍然会导致副作用,并有效地使流位置未定义。流变得无法使用。这并不能真正避免打开一个新的流的需要。 - usr
虽然这是一种非常好的方法,但我不太确定它是否回答了用户的问题。首先,将硬编码超时绑定到IO操作并不是正确的方法,因为IO性能取决于硬件。扩展方法也很好,但正如@usr所指出的那样,它不能清晰地处理IO资源。 - Stefan Z Camilleri
超时只是一个示例,用于演示扩展方法的使用。@usr是正确的,尽管限制来自于“Stream”类无法处理并发读取。扩展方法将在可以同时运行异步操作的类上无问题地工作。 - Douglas

6
没有一种通用的方法可以取消此类任务。 取消是API特定的。
例如,WebClient有一个Cancel方法。要取消未完成的调用,需要关闭SocketFileStream
Web服务客户端甚至有不同的中止调用方式。这是因为IO操作的实现者必须支持取消。
使用NetworkStream.ReadAsync并传递取消令牌可能很诱人,但是Stream.ReadAsync会将令牌丢弃,基本上不被支持。 Stream.ReadAsync只是基类方法。它本身什么也不做。具体的IO操作仅由派生类发出。这些必须本地支持取消。流无法强制执行它们。发生这种情况时,NetworkStream不支持取消。
我了解您想取消操作并保留套接字打开的意图。但是这是不可能的。(主观注释:这真是令人沮丧的事实。尤其是考虑到Windows在Win32级别支持可取消的IO。)
如果您仍然希望应用程序快速继续,即使IO操作无法取消,只需忽略该任务的结果并继续即可。请注意,最终IO可能会完成,并从套接字缓冲区中排空数据或引起其他副作用。
“通过忽略来取消”实际上使流位置未定义。流变得不可用。这并不能真正避免打开新流的需要。您仍然必须处理旧流(在大多数情况下)并重新打开。此外,您正在引入并发性。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接