Laden...

Problem: Zwei Asynchrone durchläufe - Identisch und doch verschieden

Erstellt von Ruben vor 13 Jahren Letzter Beitrag vor 13 Jahren 2.329 Views
Ruben Themenstarter:in
61 Beiträge seit 2009
vor 13 Jahren
Problem: Zwei Asynchrone durchläufe - Identisch und doch verschieden

Hallo zusammen,

Mein System:
Windows XP - SP 2
Visual Studio 2008 Team System
.Net 3.5 - Framework
etc.

Folgende Situation:

  • Eine Form hat zwei ComboBoxen.
  • Beide ComboBoxen werden asynchron gefüllt.
  • Aufruf der asynchronen Methoden läuft über einen Handler bzw. Delegaten
    und wird über BeginInvoke gestartet.
  • Um das Befüllen der ComboBoxen zu synchronisieren nutze ich AsyncOperation.
  • Die asynchrone Methode für die erste ComboBox wird im Load-Event gestartet.
  • Im Load-Event wird zuerst ein AsyncOperation im AsynOperationManager erzeugt.
  • Das AsyncOperation-Objekt wird dann an die asynchrone Methode übergeben.
  • Die erste ComboBox wird über die Post-Methode erfolgreich und asynchron gefüllt.

Das Problem:

  • Die zweite ComboBox wird im Leave-Event der ersten ComboBox gefüllt.
  • Beim Aufrufen der Post-Methode wird der Vorgang in einem anderen Thread ausgeführt und es wird eine Exception geworfen.
    (Threadübergreifender Vorgang)

Das Prinzip bei der zweiten ComboBox ist dasselbe, aber es funktioniert igendwie nicht.
Erst dachte ich, dass die Form keine Post-Aufrufe verarbeiten kann, weil ich einen Wartepunkt im Event setze - sollte ein anderer Prozess bereits laufen -, aber die Form hat nicht gewartet bzw. diesen Punkt nicht erreicht.

EDIT:
Zur Lösung springen

In der Zeit vor fünf Minuten ist Jetzt die Zukunft. Jetzt ist die Gegenwart. Die Zeit, in der ich zu erzählen begonnen habe, ist die Vergangenheit von Jetzt und die Zukunft von der Gegenwart der Zeit, fünf Minuten bevor ich zu erzählen begann.

49.485 Beiträge seit 2005
vor 13 Jahren

Hallo Ruben,

warum du AsyncOperation verwendest, ist mir unklar. Mach es besser wie in [FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke) beschrieben.

herbivore

C
2.121 Beiträge seit 2010
vor 13 Jahren

Geht es hier wirklich um so viele Einträge dass ein synchrones Befüllen nicht geht, oder dauert nur das Laden der Daten so lange?
Im zweiten Fall könntest du ja nur das Laden asynchron machen und das befüllen dann "normal", dann hast du schon das ganze Threadübergreifende mit Control nicht mehr.

T
381 Beiträge seit 2009
vor 13 Jahren

Du kannst keine Asynchronen GUI-Zugriffe machen, also auch die GUI nicht aus 2 Threads aktualisieren ohne beim eigentlichen GUI Zugriff (dem aktualisieren) wieder zu synchronisieren (BeginInvoke).

Dein vorhaben klingt komplexer als es sein müsste. Normal sieht es etwa so aus:

Thread1: Stellt Daten bereit und ruft von der GUI AddData() auf.
Thread2: Stellt Daten bereit und ruft von der GUI AddData() auf.

In der GUI ruft die AddData() Funktion dann (falls nötig) BeginInvoke auf und wird so mit dem GUI Thread synchronisiert.

Genaueres siehe herbivore Verweis.

Ruben Themenstarter:in
61 Beiträge seit 2009
vor 13 Jahren

Geht es hier wirklich um so viele Einträge dass ein synchrones Befüllen nicht geht, oder dauert nur das Laden der Daten so lange?
Im zweiten Fall könntest du ja nur das Laden asynchron machen und das befüllen dann "normal", dann hast du schon das ganze Threadübergreifende mit Control nicht mehr.

Synchron hatte ich das vorher, aber es gibt immer kurze Ladezeiten (ca. 1-2 Sek) und da ist eine "gefühlte Ewigkeit" ^^.

Und einmal danke für die anderen schnellen Antworten. 😉

Mir ist bekannt, dass man mit Control.Invoke am besten diese Synchronisierung durchführt, doch bei meinem Beispiel tretet das Problem auf, dass es beim einen mit AsyncOperation funktioniert und beim anderen nicht. Das wird mir einfach nicht klar. Ich habe auch schon gedacht, dass es daran liegt, dass im Load-Event keine threadübergreifenden Vorgänge gemeldet werden, aber das war es auch nicht, denn da wird sauber synchronisiert.

EDIT:
Habe eine Änderung gemacht und es vorerst synchron laufen lassen
Bisher keine Verzögerung, aber die Lösung für das Problem würde mich dennoch interessieren.


        private void cboProduktnummer_Leave(object sender, EventArgs e)
        {
            Produkt p = (Produkt)this.cboProduktnummer.SelectedItem;
            if (p == null)
            {
                this.cboAuftragsnummer.Items.Clear();
                this.current = null;
                return;
            }

            if (p.Equals(this.current))
                return;
            else
            {
                // synchron
                this.cboAuftragsnummer.Items.Clear();
                this.cboAuftragsnummer.Items.AddRange(p.Auftrage.Values.ToArray());
                this.current = p;

                // asynchron
                //this.AuftragComboBoxInit(p);
            }
        }

In der Zeit vor fünf Minuten ist Jetzt die Zukunft. Jetzt ist die Gegenwart. Die Zeit, in der ich zu erzählen begonnen habe, ist die Vergangenheit von Jetzt und die Zukunft von der Gegenwart der Zeit, fünf Minuten bevor ich zu erzählen begann.

Ruben Themenstarter:in
61 Beiträge seit 2009
vor 13 Jahren

Da es nicht geeignet ist Code-Dateien hochzuladen, muss ich wohl oder übel den Platz im Forum ausnutzen 😉

Ich habe jetzt den Code so gut wie möglich gekürzt. Ich hoffe übersichtlich.

EDIT:
Es handelt sich um eine ComboBox in der Autragsnummern angezeigt werden.
AuftragComboBoxInit steht dafür, dass das Eintragen der Werte in die ComboBox initiiert werden soll.
Hoffe, dass es nachvollziehbar ist ^^



        // --------------------------------------------------------- //
		// Diese Methode befüllt die zweite ComboBox NICHT erfolgreich.
		// Alle Post-Methoden werden in einem DRITTEN Thread ausgeführt.
		// --------------------------------------------------------- //
        private void AuftragComboBoxInit(Produkt p)
        {
			// INFO: GUI-Thread ID - [1]
			System.Diagnostics.Debug.Print("START - GUI-Thread: {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
			
			// ...
			
			// Erstellen einer AsyncOperation um die Vorgänge beim Befüllen der ComboBox zu synchronisieren
            AsyncOperation asyncOp = AsyncOperationManager.CreateOperation("auftragComboBox");
			
			// ...
			
			// INFO: eine weitere annonyme Methode wird definiert ^^
            MethodInvoker m =
                delegate()
                {
					// ...

					// INFO: Die ID ist definitiv anders als die vom aufrufenden GUI-Thread - [5]
					System.Diagnostics.Debug.Print("ANFANG - Thread: {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);

					// Dieses WaitHandle dient dazu, die Post-Aufrufe zu synchronisieren,
					// sonst würde die Methode schon abgelaufen sein, bevor der erste Post vom GUI-Thread verarbeitet wird.
                    System.Threading.AutoResetEvent postWait = new System.Threading.AutoResetEvent(false);

					// ...
					
                    for (int i = 0; i < number; i++)
                    {
						// ...
						
						// Post wird aufgerufen um die Daten in die ComboBox zu bringen
                        asyncOp.Post(
                            delegate(object args)
                            {
                                try
                                {
									// INFO:
									// Wenn der Wert FALSE ist, dann brauch die Aktion nicht ausgeführt werden
									//
									// IMPORTANT:
									// Hier tretet der entscheidende Fehler auf,
									// obwohl der Aufbau derselbe ist wie im ersten Durchlauf der ersten ComboBox
                                    this.cboAuftragsnummer.Items.AddRange(datenArray);
                                }
                                catch (Exception)
                                {
									// IMPORTANT:
									// Es ist eine Exception wegen eines threadübergreifenden Vorgangs aufgetreten.
									// Die ID dieses Threads ist [4]
									// (HINWEIS: [4] ist dieselbe ID wie beim Thread in "ProduktComboBoxInit()")
									// (Werden IDs also doppelt vergeben, wenn der eine Thread nicht mehr ausgeführt wird?)
                                    System.Diagnostics.Debug.Print("ERROR - Thread: {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
                                }
								
								// Signalisiert, dass der Thread seinen Vorgang fortsetzen kann.
                                postWait.Set();
                            }, null);
							
						// Thread wartet bis der Post abgeschlossen wurde.
                        postWait.WaitOne();
						
						// Kurze Pause ;)
						System.Threading.Thread.Sleep(0);
						
						// ...
                    }

                    // ...

					// OperationComplete wird mit dem letzten Post ausgeführt
                    asyncOp.PostOperationCompleted(
                        delegate(object args)
                        {
                            try
                            {
								// INFO:
								// Wenn der Wert FALSE ist, dann brauch die Aktion nicht ausgeführt werden
								//
								// IMPORTANT:
								// Hier tretet der entscheidende Fehler auf, obwohl der Aufbau derselbe ist wie in "ProduktComboBoxInit()"
                                if (lastA != null)
                                    this.cboAuftragsnummer.Items.AddRange(datenArray);
                            }
                            catch (Exception)
                            {
								// IMPORTANT: (prinzipiell dasselbe wie oben)
								// Es ist eine Exception wegen eines threadübergreifenden Vorgangs aufgetreten.
								// Die ID dieses Threads ist [4]
								// (HINWEIS: [4] ist dieselbe ID wie beim Thread in "ProduktComboBoxInit()")
								// (Werden IDs also doppelt vergeben, wenn der eine Thread nicht mehr ausgeführt wird?)
                                System.Diagnostics.Debug.Print("ERROR - Thread: {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
                            }
							
							// Signalisiert, dass der Thread seinen Vorgang fortsetzen kann.
                            postWait.Set();
                        }, null);
						
					// Thread wartet bis PostOperationCompleted abgeschlossen wurde.
                    postWait.WaitOne();
					
					// das WaitHandle wird freigegeben
					postWait.Close();
					
					// Die ID dieses Thread ist [5]
                    System.Diagnostics.Debug.Print("ENDE - Thread: {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
                };

			// Startet den asynchronen Vorgang
			m.BeginInvoke(delegate(IAsyncResult result)
			{
				m.EndInvoke(result);                    
			}, null);
			
			// ...
        }

In der Zeit vor fünf Minuten ist Jetzt die Zukunft. Jetzt ist die Gegenwart. Die Zeit, in der ich zu erzählen begonnen habe, ist die Vergangenheit von Jetzt und die Zukunft von der Gegenwart der Zeit, fünf Minuten bevor ich zu erzählen begann.

U
1.688 Beiträge seit 2007
vor 13 Jahren

Hallo,

ist denn überhaupt sichergestellt, das "AuftragComboBoxInit" (seltsamer Name, übrigens) im GUI-Thread ausgeführt wird? Erzeuge doch mal testweise die verwendete "AsyncOperation" auch im Load-Ereignis.

Ansonsten sieht der Code sehr nach [FAQ] Warum blockiert mein GUI? Abschnitt "Die Falle" aus.

Ruben Themenstarter:in
61 Beiträge seit 2009
vor 13 Jahren

ist denn überhaupt sichergestellt, das "AuftragComboBoxInit" (seltsamer Name, übrigens) im GUI-Thread ausgeführt wird?

Ja, ich habe die ThreadID am Anfang der Methode geprüft. GUI-Thread war ID 1
Die asynchrone Methode hatte die Thread ID 5.
Im Post war es dann die Thread ID 4 - also ein dritter Thread.

Erzeuge doch mal testweise die verwendete "AsyncOperation" auch im Load-Ereignis.

Die Methode kann ich leider nicht so leicht in das Load-Ereignis schieben, da sie auf den ausgewählten Wert in der ersten ComboBox reagiert.

ABER jetzt aufgepasst
Ich habe stattdessen die Methode im Load-Ereignis (der ersten ComboBox), welche vom Prinzip her dasselbe wie die gepostete Methode macht, in ein Click-Ereignis von einem Button gepackt.
Jetzt ratet mal was passiert ist...
Dasselbe Problem wie bei der zweiten ComboBox!

  • GUI-Thread ist ID 1
  • asynchrone Methode ist ID 4
  • Post-Thread ist ID 5
    (gut - die IDs sind andersrum 😉

Schlussfolgerung:
Alle asynchronen Vorgänge, die im Load-Ereignis initiiert werden, werden beim Aufruf von Post an den GUI-Thread geleitet.
Alle anderen Ereignisse (bisher getesteten zumindest) können die erstellten AsyncOperation.Post-Aufrufe nicht an den GUI-Thread leiten.

Ursache:
Aus mir nicht bekannten Gründen kann nur das im Load-Ereignis erstellte AsyncOperation-Objekt erfolgreich alle Post-Aufrufe an den GUI-Thread weiterleiten. Wenn das AsyncOperation-Objekt in einem anderen Ereignis erzeugt wird, das schließlich auch von demselben Thread, wie im Load-Ereignis ausführt wird (geprüft: ManagedThreadID;Thread.Name;ContextID), dann funktioniert der Aufruf der Post-Methode nicht bzw. er wird in einem dritten Thread ausgeführt.

Lösung:
Am beste ein AsyncOperation-Objekt als private-Field in der Form oder Klasse definieren und alle Post-Aufrufe auf dasselbe AsyncOperation-Objekt konzentrieren.
VORSICHT: Kein AsyncOperation.OperationComplete oder .PostOperationComplete aufrufen, dann wird das AsyncOperation-Objekt unbrauchbar. Am besten erst in Dispose oder Closing aufrufen.

(bitte um Korretur, falls notwendig, ansonsten werde ich dieses Thema als GELÖST setzen - wenn jemand eine Erklärung hat, dann wäre das auch sehr gut 😉

In der Zeit vor fünf Minuten ist Jetzt die Zukunft. Jetzt ist die Gegenwart. Die Zeit, in der ich zu erzählen begonnen habe, ist die Vergangenheit von Jetzt und die Zukunft von der Gegenwart der Zeit, fünf Minuten bevor ich zu erzählen begann.

U
1.688 Beiträge seit 2007
vor 13 Jahren

Ja, ich habe die ThreadID am Anfang der Methode geprüft. GUI-Thread war ID 1

Wer sagt, dass es 1 sein muss?

Welchen Wert hat b, wenn Du die folgende Zeile an den Anfang von AuftragComboBoxInit schreibst:


bool b=InvokeRequired;

Ruben Themenstarter:in
61 Beiträge seit 2009
vor 13 Jahren

Welchen Wert hat b, wenn Du die folgende Zeile an den Anfang von AuftragComboBoxInit schreibst:

  
bool b=InvokeRequired;  
  

Gut, das mit der ID war nur ein Testbeispiel, aber hier die Auflösung 😉


bool b = this.InvokeRequired;
System.Diagnostics.Debug.Print("b ist [{0}]", b);

Ergebnis:


b ist [False]

Nach der MSDN ist alles richtig.
Und die AuftragComboBoxInit-Methode wird auch direkt im ComboBox_Leave-Ereignis aufgerufen (ist mein Fehler, dass das leider nicht so ersichtlich war).

In der Zeit vor fünf Minuten ist Jetzt die Zukunft. Jetzt ist die Gegenwart. Die Zeit, in der ich zu erzählen begonnen habe, ist die Vergangenheit von Jetzt und die Zukunft von der Gegenwart der Zeit, fünf Minuten bevor ich zu erzählen begann.