【问题标题】:How to Cancel a PLINQ Query in a WinForms Application如何在 WinForms 应用程序中取消 PLINQ 查询
【发布时间】:2011-08-03 13:19:35
【问题描述】:

我正在开发处理大量文本数据的应用程序,收集有关单词出现的统计信息(请参阅:Source Code Word Cloud)。

我的代码的简化核心在做什么。

  1. 枚举所有带有 *.txt 扩展名的文件。
  2. 枚举每个文本文件中的单词。
  3. 按单词分组并计算出现次数。
  4. 按出现次数排序。
  5. 输出前 20 名。

使用 LINQ 一切正常。迁移到 PLINQ 给我带来了显着的性能提升。 但是......在长时间运行的查询期间的可取消性丢失了。

OrderBy 查询似乎正在将数据同步回主线程,并且未处理 windows 消息。

在下面的示例中,我正在根据 MSDN How to: Cancel a PLINQ Query 演示我的取消实现,但它不起作用:(

还有其他想法吗?

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace PlinqCancelability
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            m_CancellationTokenSource = new CancellationTokenSource();
        }

        private readonly CancellationTokenSource m_CancellationTokenSource;

        private void buttonStart_Click(object sender, EventArgs e)
        {
            var result = Directory
                .EnumerateFiles(@"c:\temp", "*.txt", SearchOption.AllDirectories)
                .AsParallel()
                .WithCancellation(m_CancellationTokenSource.Token)
                .SelectMany(File.ReadLines)
                .SelectMany(ReadWords)
                .GroupBy(word => word, (word, words) => new Tuple<int, string>(words.Count(), word))
                .OrderByDescending(occurrencesWordPair => occurrencesWordPair.Item1)
                .Take(20);

            try
            {
                foreach (Tuple<int, string> tuple in result)
                {
                    Console.WriteLine(tuple);
                }
            }
            catch (OperationCanceledException ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        private void buttonCancel_Click(object sender, EventArgs e)
        {
            m_CancellationTokenSource.Cancel();
        }

        private static IEnumerable<string> ReadWords(string line)
        {
            StringBuilder word = new StringBuilder();
            foreach (char ch in line)
            {
                if (char.IsLetter(ch))
                {
                    word.Append(ch);
                }
                else
                {
                    if (word.Length != 0) continue;
                    yield return word.ToString();
                    word.Clear();
                }
            }
        }
    }
}

【问题讨论】:

    标签: c# .net winforms linq plinq


    【解决方案1】:

    正如 Jon 所说,您需要在后台线程上启动 PLINQ 操作。这样,用户界面在等待操作完成时不会挂起(因此可以调用取消按钮的事件处理程序并调用取消令牌的 Cancel 方法)。 PLINQ 查询会在令牌被取消时自动取消,因此您无需担心。

    这是一种方法:

    private void buttonStart_Click(object sender, EventArgs e)
    {
      // Starts a task that runs the operation (on background thread)
      // Note: I added 'ToList' so that the result is actually evaluated
      // and all results are stored in an in-memory data structure.
      var task = Task.Factory.StartNew(() =>
        Directory
            .EnumerateFiles(@"c:\temp", "*.txt", SearchOption.AllDirectories)
            .AsParallel()
            .WithCancellation(m_CancellationTokenSource.Token)
            .SelectMany(File.ReadLines)
            .SelectMany(ReadWords)
            .GroupBy(word => word, (word, words) => 
                new Tuple<int, string>(words.Count(), word))
            .OrderByDescending(occurrencesWordPair => occurrencesWordPair.Item1)
            .Take(20).ToList(), m_CancellationTokenSource.Token);
    
      // Specify what happens when the task completes
      // Use 'this.Invoke' to specify that the operation happens on GUI thread
      // (where you can safely access GUI elements of your WinForms app)
      task.ContinueWith(res => {
        this.Invoke(new Action(() => {
          try
          {
            foreach (Tuple<int, string> tuple in res.Result)
            {
              Console.WriteLine(tuple);
            }
          }
          catch (OperationCanceledException ex)
          {
              Console.WriteLine(ex.Message);
          }
        }));
      });
    }
    

    【讨论】:

    • @Jon 感谢您的回答。它可以工作,但代码看起来很混乱。使用 PLINQ 的原因之一是线程人员的抽象。我想等待更优雅的解决方案,直到接受您的解决方案。
    • 谢谢我已经修改了这段代码。最好将相同的取消令牌传递给任务。在这种情况下,您不需要在任务中捕获 OperationCanceled 异常。最后检查任务的 .IsCanceled 和 .Exception 属性就足够了。
    【解决方案2】:

    您当前在 UI 线程中迭代查询结果。即使 查询 是并行执行的,您仍在 UI 线程中迭代结果。这意味着 UI 线程忙于执行计算(或等待查询从其其他线程获取结果)以响应单击“取消”按钮。

    您需要将遍历查询结果的工作放到后台线程上。

    【讨论】:

    • 好的,如何在另一个线程中调用取消?接受取消调用也太忙了,不是吗?太忙不是正确答案。我添加了 'Thread.Sleep(10);'到“ReadWords(字符串行)”方法。它不会有帮助。主线程只是被阻塞,等待 AsParallel 线程重新加入。
    • @gmamaladze:您可以从 UI 线程执行取消 - 您所要做的就是确保它足够空闲以响应点击!
    • @gmamaladze:让 UI 线程 sleep 仍然会阻止它响应事件。您根本不应该在 UI 线程中执行长时间运行的任务。同样,遍历查询结果的线程不必“接受”取消调用 - 这与响应点击事件根本不同。
    【解决方案3】:

    我想我找到了一些优雅的解决方案,它更适合 LINQ / PLINQ 概念。

    我正在声明一个扩展方法。

    public static class ProcessWindowsMessagesExtension
    {
        public static ParallelQuery<TSource> DoEvents<TSource>(this ParallelQuery<TSource> source)
        {
            return source.Select(
                item =>
                {
                    Application.DoEvents();
                    Thread.Yield();
                    return item;
                });
        }
    }
    

    而不是将其添加到我想要响应的任何地方。

    var result = Directory
                .EnumerateFiles(@"c:\temp", "*.txt", SearchOption.AllDirectories)
                .AsParallel()
                .WithCancellation(m_CancellationTokenSource.Token)
                .SelectMany(File.ReadLines)
                .DoEvents()
                .SelectMany(ReadWords)
                .GroupBy(word => word, (word, words) => new Tuple<int, string>(words.Count(), word))
                .OrderByDescending(occurrencesWordPair => occurrencesWordPair.Item1)
                .Take(20);
    

    效果很好!

    有关更多信息和源代码,请参阅我的帖子:“Cancel me if you can” or PLINQ cancelability & responsiveness in WinForms

    【讨论】:

    • Application.DoEvents() 是提高应用程序响应能力的相当糟糕的方法。即使它可以在很多情况下工作,如果你的代码更复杂,它也会给你带来很多麻烦(只需搜索它)
    猜你喜欢
    • 2013-01-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多