我可以使用 CancellationToken 取消 StreamReader.ReadLineAsync 吗?

Can I cancel StreamReader.ReadLineAsync with a CancellationToken?

当我通过调用 CancellationTokenSourceCancel() 方法取消具有以下内容的异步方法时,它最终会停止。然而,由于行 Console.WriteLine(await reader.ReadLineAsync()); 需要相当多的时间才能完成,我试图将我的 CancellationToken 也传递给 ReadLineAsync() (期望它传递给 return 一个空字符串)以便使该方法对我的 Cancel() 调用更敏感。但是我无法将 CancellationToken 传递给 ReadLineAsync().

我可以取消对 Console.WriteLine()Streamreader.ReadLineAsync() 的呼叫吗?如果可以,我该怎么做?

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

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

更新: 正如下面的评论所述,由于每行 40.000 个字符的格式不正确的输入字符串,仅 Console.WriteLine() 调用就已经占用了几秒钟。打破这一点解决了我的响应时间问题,但我仍然对任何关于如何取消这个 long-运行 语句的建议或解决方法感兴趣,如果出于某种原因将 40.000 个字符写入一行(例如在转储时整个字符串到一个文件中)。

您无法取消 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);
}

您无法取消操作,除非它是可取消的。您可以使用 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.

,它是瞬间的

关于准则:如果您的实现实际上不支持取消,则您不应接受令牌,因为它会发送混合消息。

如果你确实有一个巨大的字符串要写入控制台,你不应该使用 Console.WriteLine。您可以一次将字符串写入一个字符并使该方法可取消:

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

    Console.WriteLine();
}

一个更好的解决方案是分批写入而不是单个字符。这是使用 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);
}

我将此 answer 概括为:

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())
{
    . . .
}

将变为:

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

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

参见示例 http://pastebin.com/KauKE0rW

我喜欢用无限延迟,代码很干净。 如果 waiting 完成 WhenAny returns 并且 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;
        }
}

.NET 6 带来了 Task.WaitAsync(CancellationToken)。所以可以这样写:

StreamReader reader = new StreamReader(dataStream);
while (!reader.EndOfStream)
{       
    Console.WriteLine(await reader.ReadLineAsync().WaitAsync(cancellationToken).ConfigureAwait(false));
}

在.NET 7(尚未发布)中,应该可以简单地写成:

StreamReader reader = new StreamReader(dataStream);
while (!reader.EndOfStream)
{       
    Console.WriteLine(await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
}

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