4 Stimmen

Abbrechen einer PLINQ-Abfrage in einer WinForms-Anwendung

Ich arbeite an einer Anwendung, die große Mengen von Textdaten verarbeitet und Statistiken über das Vorkommen von Wörtern erstellt (siehe: Quellcode-Wortwolke ).

Der vereinfachte Kern meines Codes sieht folgendermaßen aus.

  1. Aufzählung aller Dateien mit der Erweiterung *.txt.
  2. Aufzählung der Wörter in den einzelnen Textdateien.
  3. Nach Wort gruppieren und Vorkommen zählen.
  4. Nach Vorkommen sortieren.
  5. Ausgabe Top 20.

Mit LINQ hat alles gut funktioniert. Der Wechsel zu PLINQ brachte mir einen deutlichen Leistungsschub. Aber ... die Abbrechbarkeit bei lang laufenden Abfragen ist verloren.

Es scheint, dass die OrderBy-Abfrage Daten zurück in den Hauptthread synchronisiert und Windows-Nachrichten nicht verarbeitet werden.

Im folgenden Beispiel zeige ich meine Umsetzung der Kündigung nach MSDN How to: Abbrechen einer PLINQ-Abfrage was nicht funktioniert :(

Haben Sie eine andere Idee?

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();
                }
            }
        }
    }
}

3voto

Tomas Petricek Punkte 233658

Wie Jon sagte, müssen Sie die PLINQ-Operation in einem Hintergrund-Thread starten. Auf diese Weise bleibt die Benutzeroberfläche nicht hängen, während sie auf den Abschluss der Operation wartet (so dass der Ereignishandler für die Schaltfläche Cancel aufgerufen werden kann und die Cancel Methode des Storno-Tokens aufgerufen wird). Die PLINQ-Abfrage bricht automatisch ab, wenn das Token storniert wird, Sie müssen sich also nicht darum kümmern.

Hier ist eine Möglichkeit, dies zu tun:

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);
      }
    }));
  });
}

1voto

Jon Skeet Punkte 1325502

Sie iterieren gerade über die Abfrageergebnisse im UI-Thread . Auch wenn die Abfrage parallel ausgeführt wird, iterieren Sie immer noch über die Ergebnisse im UI-Thread. Das bedeutet, dass der UI-Thread zu sehr mit der Durchführung von Berechnungen beschäftigt ist (oder darauf wartet, dass die Abfrage Ergebnisse von den anderen Threads erhält), um auf den Klick auf die Schaltfläche "Abbrechen" zu reagieren.

Sie müssen die Arbeit der Iteration über die Abfrageergebnisse auf einen Hintergrund-Thread verlagern.

-1voto

George Mamaladze Punkte 7293

Ich denke, ich habe eine elegante Lösung gefunden, die besser in das LINQ/PLINQ-Konzept passt.

Ich deklariere eine Erweiterungsmethode.

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

Und dann füge ich sie in meine Abfrage ein, wo immer ich reagieren möchte.

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);

Es funktioniert gut!

In meinem Beitrag dazu finden Sie weitere Informationen und Quellcode zum Spielen: "Cancel me if you can" oder PLINQ-Abbrechbarkeit und Reaktionsfähigkeit in WinForms

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X