我能使用CancellationToken取消StreamReader.ReadLineAsync吗?

25

当我通过调用我的的Cancel()方法取消下面内容的异步方法时,它最终会停止。但是,由于行Console.WriteLine(await reader.ReadLineAsync())需要相当长的时间才能完成,因此我尝试将我的传递给ReadLineAsync()(期望它返回一个空字符串),以便使该方法对我的Cancel()调用更加响应。然而,我无法将传递给ReadLineAsync()

我可以取消对Console.WriteLine()Streamreader.ReadLineAsync()的调用吗?如果可以,如何实现?

为什么ReadLineAsync()不接受?我认为即使在被取消后该方法仍然完成,给异步方法提供可选的参数也是一种良好的实践。

StreamReader reader = new StreamReader(dataStream);
while (!reader.EndOfStream)
{
    if (ct.IsCancellationRequested){
        ct.ThrowIfCancellationRequested();
        break;
    }
    else
    {
        Console.WriteLine(await reader.ReadLineAsync());
    }
}

更新: 如下面的评论所述,由于每行40,000个字符的输入字符串格式不正确,Console.WriteLine() 调用本身就已经花费了几秒钟。将其拆分可以解决我的响应时间问题,但我仍然对任何关于如何取消这个长时间运行的语句的建议或解决方法感兴趣,如果有某种原因需要将40,000个字符写入一行(例如将整个字符串倾销到文件中)。


3
你试图解决错误的问题。你编写了一个极其不友好的程序,它以远高于用户可读速度的速度疯狂地滚动文本。现在你正在寻找“停止这种荒谬”的解决方案。当然,你的用户也会这样做。但这不是真正的解决方案,正确的方法是从一开始就不要让它开始。将它写入文件,用记事本显示。任何东西都比这好。 - Hans Passant
你正在试图解决错误的问题。感谢您如此明确 - 您绝对是正确的。 - H W
2
真正的问题是如何取消不是即时的ReadLineAsync,类似TcpSocket。ConsoleWriteLine只是噪音。 - g.pickardou
5个回答

14
.NET 6带来了Task.WaitAsync(CancellationToken)。因此,我们可以这样书写代码:
using StreamReader reader = new StreamReader(dataStream);
while (!reader.EndOfStream)
{       
    Console.WriteLine(await reader.ReadLineAsync().WaitAsync(cancellationToken).ConfigureAwait(false));
}

在尚未发布的.NET 7中,可以简单地编写以下内容:
using StreamReader reader = new StreamReader(dataStream);
while (!reader.EndOfStream)
{       
    Console.WriteLine(await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
}

基于https://github.com/dotnet/runtime/issues/20824https://github.com/dotnet/runtime/pull/61898


是的,但在这种情况下,您不需要使用检查EndOfStream的构造。当到达流的末尾时,ReadLineAsync将返回null。(另外:将读取器放入using声明中) - JHBonarius
这是一个片段展示某些东西,它并不是为了复制粘贴到你的生产代码中而编写的。确实,它应该更好,并且我添加了使用语句。请随时直接提出任何改进意见。谢谢。 - MartyIX
可能是,但你知道有多少人实际上会复制粘贴 Stack Overflow 上的代码... - JHBonarius

13

只有在可取消的情况下,您才能取消操作。您可以使用WithCancellation扩展方法让您的代码流像被取消一样运行,但底层仍然会运行:

public static Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    return task.IsCompleted // fast-path optimization
        ? task
        : task.ContinueWith(
            completedTask => completedTask.GetAwaiter().GetResult(),
            cancellationToken,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
}

用法:

await task.WithCancellation(cancellationToken);

你不能取消Console.WriteLine,也不需要这么做。如果你有一个合理大小的string,那么它的输出是瞬间完成的。

关于指南:如果你的实现实际上不支持取消操作,那么你不应该接受一个token,因为这会发出混淆信息。

如果你要写入大量字符串到控制台,你不应该使用Console.WriteLine。你可以逐个字符地写入字符串,并使该方法可取消:

public void DumpHugeString(string line, CancellationToken token)
{
    foreach (var character in line)
    {
        token.ThrowIfCancellationRequested();
        Console.Write(character);
    }

    Console.WriteLine();
}

一个更好的解决方案是批量写入而不是单个字符。下面是使用MoreLinqBatch实现:

一个更好的解决方案是批量写入而不是单个字符。下面是使用MoreLinqBatch实现:

public void DumpHugeString(string line, CancellationToken token)
{
    foreach (var characterBatch in line.Batch(100))
    {
        token.ThrowIfCancellationRequested();
        Console.Write(characterBatch.ToArray());
    }

    Console.WriteLine();
}

因此,总之:

var reader = new StreamReader(dataStream);
while (!reader.EndOfStream)
{
    DumpHugeString(await reader.ReadLineAsync().WithCancellation(token), token);
}

@HW,如果不支持取消功能的话,你是无法取消Console.WriteLine的,但你可以逐步使用Console.Write。请看我的更新。 - i3arnon
2
你确定 Task.ContinueWith 也会取消整个任务吗?从我的角度来看,Task.ContinueWith 在这里不太合适。你需要像这个例子中使用的 Task.WhenAny(task, infiniteCancellableTaks) 这样的东西,其中 var infiniteCancellableTaks = Task.Delay(Timeout.Infinite, token),链接在这里 https://dev59.com/r3_aa4cB1Zd3GeqP11UN#23473779 - neleus
1
@neleus 是的。它不会取消原始任务(因为你不能这样做),但它会取消继续执行的任务。随意尝试一下。 - i3arnon
你是对的,它可以工作。对我来说看起来不太明显,但这是离题了。 - neleus
@neleus 这只是因为我们习惯了整个 Task.Delay 的 hack。使用一个 continuation,它要么在任务完成时完成,要么在令牌被取消时被取消,实际上更加优雅和合理。 - i3arnon
1
同意使用 Task.ContinueWith 更好,之前不知道还有这种解决方案。 - neleus

3

我将这个回答概括为以下内容:

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken, Action action, bool useSynchronizationContext = true)
{
    using (cancellationToken.Register(action, useSynchronizationContext))
    {
        try
        {
            return await task;
        }
        catch (Exception ex)
        {

            if (cancellationToken.IsCancellationRequested)
            {
                // the Exception will be available as Exception.InnerException
                throw new OperationCanceledException(ex.Message, ex, cancellationToken);
            }

            // cancellation hasn't been requested, rethrow the original Exception
            throw;
        }
    }
}

现在您可以在任何可取消的异步方法上使用您的取消标记,例如 WebRequest.GetResponseAsync:

var request = (HttpWebRequest)WebRequest.Create(url);

using (var response = await request.GetResponseAsync())
{
    . . .
}

will become:

var request = (HttpWebRequest)WebRequest.Create(url);

using (WebResponse response = await request.GetResponseAsync().WithCancellation(CancellationToken.None, request.Abort, true))
{
    . . .
}

See example http://pastebin.com/KauKE0rW


2
在这个例子中存在一种竞争情况。如果你在 catch 处理程序中检查 IsCancellationRequested,你就不知道取消或异常哪一个先发生了。你应该检查异常类型,以确定它是否真的是一个取消异常。 - Kenneth

1

我喜欢使用无限延迟,代码非常干净。 如果waiting完成,则WhenAny返回并且cancellationToken会抛出异常。否则,将返回task的结果。

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
        using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
        {
            var waiting = Task.Delay(-1, delayCTS.Token);
            var doing = task;
            await Task.WhenAny(waiting, doing);
            delayCTS.Cancel();
            cancellationToken.ThrowIfCancellationRequested();
            return await doing;
        }
}

-3

你不能取消 Streamreader.ReadLineAsync()。在我看来,这是因为读取单行应该非常快。但是你可以通过使用一个单独的任务变量轻松地防止 Console.WriteLine() 发生。

检查 ct.IsCancellationRequested 也是多余的,因为只有在请求取消时,ct.ThrowIfCancellationRequested() 才会抛出异常。

StreamReader reader = new StreamReader(dataStream);
while (!reader.EndOfStream)
{
    ct.ThrowIfCancellationRequested();
    string line = await reader.ReadLineAsync());

    ct.ThrowIfCancellationRequested();    
    Console.WriteLine(line);
}

谢谢您的提示,我不需要检查 IsCancellationRequested!不幸的是,在两个任务之间再次检查取消几乎没有减少我的响应时间。 - H W
7
如果你正在阅读标准输出流或网络流,即任何内容不能一次全部实现的流,则读取单行可能非常缓慢。 - dcstraw
3
如上所述,ReadLineAsync()可能需要很长时间,例如在TCP连接的情况下,服务器线程正在等待客户端发送一行文本。为了让翻译更加通俗易懂,可以这样表述:就像之前提到的那样,ReadLineAsync()可能需要很长时间,特别是在TCP连接的情况下,此时服务器线程会一直等待客户端发送一行文本。 - user1595443

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