【问题标题】:Reading file asynchronously along with CancellationToken与 CancellationToken 一起异步读取文件
【发布时间】:2020-08-09 09:39:21
【问题描述】:

我在 WinForms 中有一个从文本文件导入的按钮。一旦开始导入,它将按钮的文本更改为Processing... 并实例化一个 CancellationToken,如果用户请求,它应该取消操作。如果在导入过程中再次按下按钮,它应该会激活任务取消。

有很多方法可以做到这一点,我无法决定使用哪一种。最干净的方法是什么?

private CancellationTokenSource _cts;
private List<string> _list = new List<string>();

private async void Button_Click(object sender, EventArgs e)
{
    if (button.Text == "Processing...")
    {
        if (_cts != null)
        {
            _cts.Cancel();
            _cts.Dispose();
        }
    }
    else
    {
        using var dialog = new OpenFileDialog
        {
            Filter = "All Files (*.*)|*.*"
        };

        if (dialog.ShowDialog() == DialogResult.OK)
        {
            button.Text = "Processing...";
            
            _cts = new CancellationTokenSource();

            var fileStream = dialog.OpenFile();

            using var reader = new StreamReader(fileStream);

            try
            {
                int count = 0;
                while (!reader.EndOfStream)
                {
                    var line = await reader.ReadLineAsync().WithCancellation(_cts.Token).ConfigureAwait(false);

                    Trace.WriteLine(line);
                    _list.Add(line);

                    count++;
                }
            }
            catch (TaskCanceledException)
            {
            }

            //await Task.Run(async () =>
            //{
            //    int count = 0;
            //    while (!reader.EndOfStream)
            //    {
            //        var line = await reader.ReadLineAsync().ConfigureAwait(false);
            //        //var line = reader.ReadLine();

            //        Trace.WriteLine(line);

            //        count++;
            //    }
            //});

            //await Task.Factory.StartNew(() =>
            //{
            //    int count = 0;
            //    while (!reader.EndOfStream)
            //    {
            //        var line = reader.ReadLine();

            //        Trace.WriteLine(line);

            //        count++;
            //    }
            //});

            BeginInvoke(new Action(() => button.Text = "Process"));
        }
    }
}

public static class ThreadExtensionMethods
{
    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);
    }
}

【问题讨论】:

  • 如果你只是使用var fileLines = await File.ReadAllLinesAsync(dialog.FilePath, _cts.Token);呢?
  • 阅读需要多少时间迫使您申请CancellationToken
  • @CamilioTerevinto 几个注意事项:1)它是 .NET Core API 2)它是 broken API
  • 每行有多大?读取线路时是否需要CancellationToken
  • List 不是线程安全的。我现在将答案更改为List,但请确保在加载数据时您没有对其进行任何操作。

标签: c# task


【解决方案1】:

由于FileStream.ReadAsyncslow,我不会推荐它。它是整个 async 文件系统 API 的基础方法。让我们在Task 中运行它。

private CancellationTokenSource _cts;
private List<string> _list = new List<string>();

private async void button1_Click(object sender, EventArgs e)
{
    if (_cts != null)
    {
        _cts.Cancel();
    }
    else
    {
        using var dialog = new OpenFileDialog
        {
            Filter = "All Files (*.*)|*.*"
        };

        if (dialog.ShowDialog() == DialogResult.OK)
        {
            button.Text = "Processing...";
            using (_cts = new CancellationTokenSource())
            {
                await Task.Run(() =>
                {
                    using var reader = new StreamReader(dialog.OpenFile());
                    int count = 0;
                    while (!reader.EndOfStream && !_cts.Token.IsCancellationRequested)
                    {
                        var line = reader.ReadLine();

                        Trace.WriteLine(line);
                        _list.Add(line);

                        count++;
                    }
                });
            }
            _cts = null;
            button.Text = "Process";
        }
    }
}

请注意,List 不是线程安全的。确保在加载数据时您没有对其进行任何操作。如果这样做,请考虑从 System.Collections.Concurrent 命名空间收集一些集合。

您也可以在此处避免代理收集_list.Add(line) 并内联处理数据或将其添加到ListBox,如您在 cmets 中所说。像这样的东西:this.Invoke((Action)(() =&gt; listBox.Add(line)))

【讨论】:

  • 使用ConcurrentBag而不是List的原因是什么?
  • @TheodorZoulias 因为它在池化的Thread 上运行。猜测 OP 想在文件加载时读取集合。如果没有,List 是适用的。
  • @TheodorZoulias 同意。在 cmets 中询问它的详细信息。
  • 完全不需要Interlocked.Increment(ref count);,这段代码不会同时在多个线程中运行。使用 Concurrent 集合毫无意义的相同原因。并且没有必要仅仅为了捕获异常而对异常不做任何事情,只需完成方法并避免性能损失。
  • @CamiloTerevinto 为什么我认为 count 是一些全局变量?不知道,修好了。这里不需要例外,但它会在Cancel() 上出现一次。因此,根本不会对性能产生影响。但这是有道理的,也解决了这个问题。谢谢!
猜你喜欢
  • 1970-01-01
  • 2019-02-20
  • 2021-08-07
  • 2019-01-13
  • 2012-12-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多