| 

.NET C# Java Javascript Exception

9
Hallo zusammen,

ich habe endlich mal Zeit gefunden, mich mit der Task Parallel Library zu beschäftigen. Ich möchte eine größere Datenmenge im Hintergrund laden und dem Benutzer die Möglichkeit bieten, die Operation abzubrechen. Ich habe mir eine ganze Reihe Beispiel angeschaut und ausprobiert. In allen Beispielen war der UI-Thread eingefroren, nachdem der Task gestartet wurde. Somit hat der Benutzer keine Chance, den Cancel-Button zu drücken. Die einzige Möglichkeit, die ich gefunden habe war, den Task mittels Thread.Sleep() regelmäßig kurzzeitig schlafen zu legen. Das erinnert mich doch sehr an das gute alte DoEvents. Ich habe hier eine kurze Demo meiner Implementierung:

public partial class Form1 : Form
{
// create the cancellation token source
private CancellationTokenSource cts;

private void btnDoIt_Click(object sender, EventArgs e)
{
this.cts = new CancellationTokenSource();
TaskScheduler ui_scheduler = TaskScheduler.FromCurrentSynchronizationContext();

Cursor = Cursors.WaitCursor;
this.btnDoIt.Enabled = false;

var ints = new List<int>();
int number = 100000;
Task task1 = Task.Factory.StartNew(() =>
{
for(int i = 0; i < number; i++)
{
ints.Add(i);
int i1 = i;
//Ohne diese Zeile ist kein Canceln des Tasks möglich
Thread.Sleep(1);
this.cts.Token.ThrowIfCancellationRequested();
}
},
this.cts.Token,
TaskCreationOptions.None,
TaskScheduler.Default);

task1.ContinueWith(t =>
{
if(t.IsCanceled)
{
MessageBox.Show(string.Format("Task {0} Canceled", t.Id));
}
else if(t.IsFaulted)
{
MessageBox.Show(string.Format("Task {0} Faulted", t.Id));
}
else
{
this.lstBox.DataSource = ints;
}
Cursor = Cursors.Default;
this.btnDoIt.Enabled = true;
},
ui_scheduler);
}

private void btnCancel_Click(object sender, EventArgs e)
{
// cancel the second token source
this.cts.Cancel();
}
}


Gibt es wirklich keine einfache und elegante Möglichkeit ohne Thread.Sleep auszukommen? Das neue async Pattern kann ich leider nicht verwenden, da ich auf .NET 4.0 festgelegt bin und mein Kunde nicht wünscht dass ich Pre-Release Software verwende (ansonsten hätte ich die async CTP für Visual Studio 2010 ausprobiert).

Vielen Dank im Voraus für eure Tips
Klaus
18.07.2013
luedi 2,0k 1 9
4 Antworten
3
Vielleicht kann man nur nicht canceln, weil der Task so schnell fertig wird? Dann wird wohl die meiste Zeit damit verbracht, die 100k ints in die (langsame) ListBox zu laden. Lade mal zum Test nur die ersten 10 in die ListBox.
18.07.2013
Marvin Steppat 4,0k 1 4 8
Der Code ist ja nur ein Beispiel. Wenn ich die Arraygröße groß genug definiere kann ich sehen, dass der UI-Thread blockiert ist (genauso, als ob ich mit task1.Wait() auf die Beendigung des Tasks warten würde)
luedi 18.07.2013
1
Weil die ganze Zeit in "this.lstBox.DataSource = ints;" verbracht wird, was auf dem UI-Thread abläuft. Pausiere mal den Debugger während die Anwendung hängt - vermutlich steckt sie in dieser Zeile.
Marvin Steppat 18.07.2013
Nein, die Zuweisung an den Datasource wird nur einmal ausgeführt. ".ContinueWith()" wird ja auch erst dann gestartet, wenn "task1" beendet ist (egal ob abgebrochen oder durchgelaufen.
luedi 18.07.2013
In welcher anderen Zeile steckt die Anwendung denn dann fest wenn Du pausierst? Wenn der UI-Thread blockiert ist, muss er ja gerade irgendeine Zeile ausführen.
Marvin Steppat 18.07.2013
Wenn ich keinen Breakpoint setze steht der Debugger auf "Application.Run(Form1)". Wenn ich einen Breakpoint bei Thread.Sleep() setze, wird dieser regelmäßig angesprungen.
luedi 18.07.2013
Guck mal in den Callstack. Die Anwendung ist nicht in Application.Run sondern in Wirklichkeit viel tiefer. Evtl. musst Du auch "Show External Code" anschalten. Außerdem probier mal meinen Vorschlag, nur die 10 ersten Items zu laden (Take(10)).
Marvin Steppat 18.07.2013
Leider ist der Callstack zu lang, um ihn in den Kommentar einzufügen, aber er deutet darauf hin, dass die Message-Loop des Hauptformulars läuft.
luedi 18.07.2013
Dann teste doch bitte auch den anderen Vorschlag. Es kostet dich nur eine Minute und löst vllt. dein Problem.
Marvin Steppat 18.07.2013
1
Habe es mal eben getestet. Das ist tatsächlich das Problem. Der Call Stack ist tatsächlich nicht aussagekräftig gewesen. Die ListBox ist für diese Menge Items zu langsam. Der Task completed fast sofort.
Marvin Steppat 18.07.2013
wie schon kommentiert. So ähnlich habe ich angefangen, mit dem selben Ergebnis.

Ich habe die Solution auf meinem Skydrive bereitgestellt. Der LInk dorthin ist: http://sdrv.ms/1bLjTAN.
luedi 18.07.2013
Wenn ich den Sleep-Aufruf entferne und "this.lstBox.DataSource = ints.Take(10).ToList();" sage, dann läuft alles wie es soll. Oder hab ich was falsch verstanden?
Marvin Steppat 18.07.2013
1
Warum nutzt Du nicht die generische Variante von Task. In ContinueWith dann das Result an die ListBox übergeben. Im Grunde genommen springst Du bei jedem Add in den UI-Thread und dann wieder zurück.

var task = Task<IEnumerable<int>>.Factory.StartNew(() => Enumerable.Range(0,1000) /* bzw. deine Code */ );
task.ContinueWith(t => { listBox1.DataSource = t.Result; });
18.07.2013
lbm1305 849 1 8
So ähnlich sah mein Beispielcode ursprünglich aus (insbesondere der Block in .ContinueWith). Das Ergebnis war aber das gleiche, der Cancel-Button auf dem Formular war nicht ansprechbar.
luedi 18.07.2013
1
Die Task task1 in Deinem Beispiel wird - wie Marvin bereits angemerkt hat - sehr schnell abgearbeitet und ohne ein Sleep(1) kannst Du sie nicht abbrechen. Das liegt daran, dass Sleep(1) einen Kontextswitch veranlasst, was Deine Task effektiv an dieser Stelle in jedem Durchlauf unterbricht. Du erzwingst also eine Preemption, Deine MessageLoop kann abgearbeitet werden und ein Cancel funktioniert, weil Du ihm in jedem Schleifendurchlauf einmal die Chance dazu gibst. Prinzipiell entspricht das einem DoEvents.

Damit ist Dein Problem nicht die nebenläufige Ausführung, sondern die ListBox. 100K Items via DataSource in die WinForms ListBox zu laden, dauert eben. Da Du eingangs erwähnt hast, dass Du Dich mit der TPL beschäftigen möchtest, gehe ich davon aus, dass es sich hier um ein konstruiertes Sample handelt und nicht um ein produktives Problem, das Du lösen musst. Sollte es doch ein Produktivproblem sein, stellst Du vlt. besser eine weitere Frage zur Optimierung von ListBox Ladevorgängen, da das mit dieser nur indirekt zu tun hat (dann findet man es besser).

Randbemerkung:
Das Async CTP hat seinen Weg in ein Release gefunden und heißt Async Targetting Pack. Es ist meines Wissens keine Pre-Release Software mehr.
18.07.2013
ffordermaier 8,4k 3 9
Danke für den Tip mit der Async CTP. Wenn ich jedoch Admin-Rechte brauche, um das Async Targeting Pack zu installieren, kann ich das knicken, da ich beim Kunden nur Hauptbenutzerrechte auf meinem Entwickler-Rechner habe.
luedi 19.07.2013
0
Vielen Dank für eure rege Beeiligung. Ich möchte eure Antworten und Kommentare zu einem Fazit für mich zusammenfassen.

In der Praxis habe ich vor, Datenbankzugriffe asynchron durchzuführen (allerdings nicht mit ADO.NET sondern mit Hilfe von C-Stubs über embedded SQL, und das auch nicht zum Spass, sondern weil ich muss (Thema: Altlasten :-) )), die z.T. mehrere Minuten dauern.

Euren Antowrten und Kommentaren entnehme ich, dass ich für meine Tests ein schlechtes Beispiel gewählt habe. Mir ist auch bewusst, dass die Schleife sehr schnell durchlaufen wird, ich habe allerdings auch mit 1.000.000.000 Schleifendurchläufen getestet und bin davon ausgegangen, dass ich dann eine Chance habe, schnell genug zu canceln.

Ich werde jetzt mit der konkreten Implementierung meiner Anforderungen beginnen und dann werde ich ja sehen, wie es mit dem Canceln klappt. Wenn alle Stricke reißen, kann ich den Thread.Sleep() ja immer noch einbauen.

Nochmals Danke
Klaus
19.07.2013
luedi 2,0k 1 9
Ich kann nur für mich sprechen. Wenn ich eine Aktion abbreche, dann nur, weil diese zu lang dauert. Und nur wegen des Abbrechens implementiere ich keine künstliche Pause.
lbm1305 19.07.2013

Stelle deine .net-Frage jetzt!
TOP TECHNOLOGIES CONSULTING GmbH