Laden...

Control.BeginInvoke und Racing Conditions

Erstellt von Löwenherz vor 17 Jahren Letzter Beitrag vor 17 Jahren 7.396 Views
L
Löwenherz Themenstarter:in
58 Beiträge seit 2006
vor 17 Jahren
Control.BeginInvoke und Racing Conditions

Hallo Leute 🙂

Ich habe da ein ganz besonderes Leckerli, an dem ich schon ein paar Stunden rumlaboriere ^^ Ich starte mit Socket.BeginConnect() zwei Verbindungsversuche zu einem Host im Netzwerk. Die Callback-Methode, die bei Erfolg oder Misserfolg aufgerufen wird, läuft in einem Thread des Threadpools, wenn ich das recht verstanden habe. Da meine GUI aber auch etwas davon mitbekommen soll, habe ich in dieser Callback-Methode Control.BeginInvoke() benutzt, um meinen GUI-Thread über den Ausgang des Verbindungsaufbaus zu informieren.

An Anfang und Ende dieser Methode, die im Context des GUI-Threads läuft, habe ich eine Meldung eingebaut, die dann ins Log geschrieben wird. Lustigerweise kommt folgendes dabei raus:

ANFANG
ANFANG
ENDE
ENDE

d.h. der GUI-Thread unterbricht irgendwo in der Mitte der Methode die Ausführung und "beginnt von vorn". Irgendwann läuft die erste Ausführung dann weiter. Das ist doch völlig unmöglich, wenn ich Control.BeginInvoke() zum Aufruf dieser GUI-Methode benutze, oder?

Nebenbei... Mit Constrol.Invoke(), statt mit control.BeginInvoke() läuft die Kiste... seeeehr seltsam.

Ich hoffe, jemand von euch hatte so ein Problem schon einmal und kann mir sagen, woran es liegt.

Vielen Dank schon einmal im vorraus!

3.170 Beiträge seit 2006
vor 17 Jahren

Hallo,
wen ich das jetzt richtig verstanden habe passiert folgendes:

Ich starte mit Socket.BeginConnect() zwei Verbindungsversuche

Durch den asynchronén Connect-Aufruf werden 2 Threads gestartet, die jeweils versuchen sich zu verbinden.
Jeder dieser Threads ruft dann Dein Callback-Funktion. In dieser wird wiederum mit "BeginInvoke" ein asynchroner Aufruf gestartet, so daß die Methode, die über BeginInvoke ausgeführt wird, zwiemal gerufen wird.
Da aber bereits das BeginConnect asynchron ausgeführt wird, kommt es vermutlich zu folgender (oder ähnlicher) Reihenfolge bei der Ausführung

BeginConnect1
Callbackaufruf1 -> ruft BeginInvoke -> Ausgabe ANFANG
BeginConnect2
Callbackaufruf2 -> ruft BeginInvoke -> Ausgabe ANFANG
Ende Invoke1 -> Ausgabe ENDE
EndeCallback1
Ende Invoke2 -> Ausgabe ENDE
EndeCallback2

Musst Du dich eigentlich gleich 2mal connecten oder könntest du evtl. die beiden Connect synchron hintereinander ausführen?

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

S
8.746 Beiträge seit 2005
vor 17 Jahren

Ich denke die Beschreibung trifft es. Sehe auch nichts ungewöhnliches am Verhalten.

Inwiefern läuft es bei BeginInvoke() "merkwürdig"? BeginInvoke() stellt den Aufruf des GUI-Delegaten nur in die Windows-Warteschlange und kehrt sofort zurück. Ansonsten hast du über Invoke() zwangsläufig eine Art Synchronisation der beiden Socket-Threads , d.h. der spätere Thread muss u.U. ein wenig warten, bis er seine GUI-Aktualsierung los wird.

L
Löwenherz Themenstarter:in
58 Beiträge seit 2006
vor 17 Jahren

Erst einmal vielen Dank für eure Antworten!

Original von MarsStein
Hallo,
wen ich das jetzt richtig verstanden habe passiert folgendes:

Ich starte mit Socket.BeginConnect() zwei Verbindungsversuche
Durch den asynchronén Connect-Aufruf werden 2 Threads gestartet, die jeweils versuchen sich zu verbinden.
Jeder dieser Threads ruft dann Dein Callback-Funktion. In dieser wird wiederum mit "BeginInvoke" ein asynchroner Aufruf gestartet, so daß die Methode, die über BeginInvoke ausgeführt wird, zwiemal gerufen wird.
Da aber bereits das BeginConnect asynchron ausgeführt wird, kommt es vermutlich zu folgender (oder ähnlicher) Reihenfolge bei der Ausführung

BeginConnect1
Callbackaufruf1 -> ruft BeginInvoke -> Ausgabe ANFANG
BeginConnect2
Callbackaufruf2 -> ruft BeginInvoke -> Ausgabe ANFANG
Ende Invoke1 -> Ausgabe ENDE
EndeCallback1
Ende Invoke2 -> Ausgabe ENDE
EndeCallback2

Musst Du dich eigentlich gleich 2mal connecten oder könntest du evtl. die beiden Connect synchron hintereinander ausführen?

Hmm, so habe ich das anfangs auch gedacht. Aber wie svenson auch geschrieben hat, wird die Ausführung dieser beiden aufgerufenen Delegaten im GUI Thread nacheinander ausgeführt. Daher müsste meines Erachtens nach doch zuerst der erste Aufruf im GUI-Thread komplett durchlaufen, bevor der zweite Aufruf abgearbeitet werden kann. Ich habe gelesen, dass BeginInvoke und Invoke mit der API-Funktion PostMessage() implementiert sind und daher ja auch nacheinander ausgeführt werden müssten. Aber genau das scheint nicht zu klappen. Sehr seltsam ... nachdenklich am Kopf kratz

3.170 Beiträge seit 2006
vor 17 Jahren

Hallo Löwenherz,

Aber wie svenson auch geschrieben hat, wird die Ausführung dieser beiden aufgerufenen Delegaten im GUI Thread nacheinander ausgeführt. Daher müsste meines Erachtens nach doch zuerst der erste Aufruf im GUI-Thread komplett durchlaufen, bevor der zweite Aufruf abgearbeitet werden kann.

das hat svenson so aber nicht geschrieben, und das ist eben genau der Unterschied zwischen BeginInvoke()und Invoke(). die Invoke()-Methode läuft synchron im GUI-Thread und blockiert diesen bis Ende seiner Ausführung. Die BeginInvoke()-Methode läuft aber asynchron und ist nicht an den GUI-Thread gebunden und hält diesen auch nicht auf.

EDIT:
Durch die ganzen asynchronen Aufrufe läuft bei Dir so ziemlich alles nebeneinander her ab. Das macht die Geschichte relativ unübersichtlich undschwierig in der Handhabe. Vielleicht solltest Du Dir mal überlegen, ob das in Deinem Programm wirklich in der Art nötig ist.

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

L
Löwenherz Themenstarter:in
58 Beiträge seit 2006
vor 17 Jahren

Ja, ist leider nötig, da ich meinen GUI-Thread unter keinen Umständen blockieren darf.

Und unübersichtlich finde ich das auch irgendwie nicht. Eigentlich ist es doch ganz einfach. Ich verstehe nur nicht, wieso das nicht so funktioniert g

  • BeginConnect() startet einen Thread aus dem Threadpool und lässt die Connect-Operation darin laufen
  • Callback-Methode wird von diesem Thread aufgerufen
  • Innerhalb der Callback-Methode packe ich mit BeginInvoke() eine Message in die Messagequeue von meinem GUI-Thread
  • GUI-Thread arbeitet die Message irgendwann ab, wenn sie dran ist.

Das sieht für mich doch nach einer definierten Reihenfolge aus. Durch das Posten einer Message in BeginInvoke() sequentialisiert sich doch die gesamte Ausführung. Zumindest meinem Verständnis nach 😉

Bitte korrigiert mich, wenn ich da falsch liege.

3.170 Beiträge seit 2006
vor 17 Jahren

Hallo,
ich denke Du liegst immer noch verkehrt... Wenn Du Methoden asynchron aufrufst, z.B. durch BeginInvoke(), dann werden die nicht von einem anderen Thread irgendwann abgearbeitet, sondern die Abarbeitung wird sozusagen nur angestossen und läuft parallel zum anstossenden Thread ab. das heißt du kehrst mit BeginInvoke() direkt zurück in den aufrufenden Thread.

Um sicherzustellen daß Deine Funktion dann "an einemStück" abgearbeitet wird musst du die asynchronen Aufrufe synchronisieren, das kannst Du zum Beispielmachen indem Du Deinen ganzen Funktionsrumpf in ein lock(this){}-Block packst. Dann machst Du aber asynchrone Aufrufe die Du von Hand wieder "zurücksynchronisierst" sozusagen. Wenn Du haben willst, daß die Funktion im GUI-Thread abgearbeitet wird, ist eher Invoke() DeinKandidat, und nicht BeginInvoke.

Noch einmal die Frage: Wenn die Funktion vom GUI-Zhread abgearbeitet werden soll, und erst danach der nächste Aufruf, wozu dann die asynchronen Aufrufe?

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

L
Löwenherz Themenstarter:in
58 Beiträge seit 2006
vor 17 Jahren

Die asynchronen Aufrufe mit BeginConnect() habe ich genommen, damit mir mein GUI-Thread nicht blockiert, wenn er keinen Connect hinbekommt und irgendwann mit einem Timeout wiederkommt. Die Applikation sieht dann ziemlich abgestürzt aus, was sie aber nicht ist.

Und das Control.BeginInvoke() wollte ich eigentlich nur mal ausprobieren und war verwundert, wieso es nicht ging 😁 Verstanden habe ich das mit Problem mit Control.BeginInvoke() leider immer noch nicht.

Ich habe mir das Ding hier mal durchgelesen:
http://www.codeproject.com/csharp/begininvoke.asp

... und musste feststellen, dass die Callback-Methoden der Socket.BeginConnect()-Methode mit Control.BeginInvoke() im GUI-Thread per Window-Message die gewünschte Methode anstoßen sollten. Haben sie ja auch, nur leider war die ganze Geschichte dann nicht mehr synchron und ich weiß leider nicht, wie ich die Thread-ID rausbekomme, sonst hätte ich da einfach mal ein Debug.WriteLine() mit der Thread-ID reingebastelt, um rauszubekommen, wer denn da meine GUI-Methode aufruft ^^

3.170 Beiträge seit 2006
vor 17 Jahren

Hallo,

nur leider war die ganze Geschichte dann nicht mehr synchron

wie denn auch? Du hast es ja explizit asynchron aufgerufen!! Wenn Du das wieder synchron habe willst, verweise ich nochmal auf die Verwendung von lock (siehe mein letzter Post).

und ich weiß leider nicht, wie ich die Thread-ID rausbekomme

mit Thread.CurrentThread kommst Du immer an den gerade laufenden Thread.

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

4.207 Beiträge seit 2003
vor 17 Jahren

Original von MarsStein
[...] das kannst Du zum Beispielmachen indem Du Deinen ganzen Funktionsrumpf in ein lock(this){}-Block packst [...]

Bitte kein lock(this), das ist ganz schlechter Stil und unsicher.

Wissensvermittler und Technologieberater
für .NET, Codequalität und agile Methoden

www.goloroden.de
www.des-eisbaeren-blog.de

3.170 Beiträge seit 2006
vor 17 Jahren

Hallo,
sorry Du hast natürlich recht ich würde normalerweise auch jemand anderen als ausgerechnet this locken war nur als Beispiel gedacht... ⚠
Aber ich würde bei diesem Problem hier eher schauen ob ich nicht was am Aufbau ändern kann, damit nicht unbedingt alles asynchron läuft, dann kommts möglicherweise nicht so weit daß man hier überhaupt ein lock braucht.

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

3.728 Beiträge seit 2005
vor 17 Jahren

Original von Golo Haas
Bitte kein lock(this), das ist ganz schlechter Stil und unsicher.

lock(this) sperrt doch alle Instanzvariablen für gleichzeitigen Thread-Zugriff, oder? Was ist daran so schlecht?

3.170 Beiträge seit 2006
vor 17 Jahren

@Rainbird:

Ein mögliches Problem mit lock(this) ist z.B. folgendes:

class MyClass
{
  public void MyMethod()
  {
    lock(this)
   {
      //Arbeit machen
    }
  }
}

// dann in einer anderen Methode z.B.:
MyClass myclass  = new MyClass();
lock(myClass)
{
  myClass.MyMethod();
}

Deadlock, isn't it ??
Ist also ziemlich gefährlich...

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

3.728 Beiträge seit 2005
vor 17 Jahren
Verstehe

Ok, verstehe. Ich verwende lock(this) gerne bei Objekten, die mittels Remoting als Singleton veröffentlicht werden. Da jeder Clientaufruf als separater Thread eingeht, muss ich natürlich dafür sorgen, dass die Instanzvariablen konsistent sind. Eine Verschachtelung, wie Du sie gezeigt hast, kann dort eigentlich nicht vorkommen. Bei Multithreading in GUI werde ich aber lock(this) zukünftig vermeiden. Danke für die Erklärung.

3.170 Beiträge seit 2006
vor 17 Jahren

Hab den Artikel aus der MSDN zu Threadsynchronisierung mal rausgesucht:

ms-help://MS.NETFramework.v20.de/dv_csref/html/413e1f28-a2c5-4eec-8338-aa43e7982ff4.htm

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

L
Löwenherz Themenstarter:in
58 Beiträge seit 2006
vor 17 Jahren

Original von MarsStein
Hallo,

nur leider war die ganze Geschichte dann nicht mehr synchron
wie denn auch? Du hast es ja explizit asynchron aufgerufen!! Wenn Du das wieder synchron habe willst, verweise ich nochmal auf die Verwendung von lock (siehe mein letzter Post).
und ich weiß leider nicht, wie ich die Thread-ID rausbekomme
mit Thread.CurrentThread kommst Du immer an den gerade laufenden Thread.

Hmm, ja ^^

Die Asynchronität sollte meines Erachtens nach beim Control.BeginInvoke() enden, da das ja eine Window-Message in die Messagequeue des GUI-Threads postet. Das versuche ich ja die ganze Zeit klarzumachen. So steht es zumindest bei
http://www.codeproject.com/csharp/begininvoke.asp

Und mit Thread.CurrentThread hast du schon recht, aber da komme ich höchstens an einen Namen mittels Thread.Name. Eine ID gibt es leider nicht. Wenn du mir aber sagen kannst, wie ich einem Thread aus dem Threadpool einen Namen zuordne, wäre ich aus dem Schneider 🙂

T
512 Beiträge seit 2006
vor 17 Jahren

Du verstehst da glaub ich Synchronisation falsch.

Die Nachrichten werden in den GUI Thread auch genau in der Reihenfolge ausgewertet wie du sie reinsteckst. Nur hat das mit Synchronisation herzlich wenig zu tun.

Genau das passiert bei dir auch.

Synchronisation heißt grob gesagt, dass ein Thread auf einen anderen wartet.

Und genau das ist das Problem bei dir.

Das Geschehen läuft in 2 Threads ab, keiner wartet auf den anderen.
Der eine startet ohne auf das Beenden des Anderen zu warten. Also kommt der Aufruf zum Start des 2. vor dem beenden des 1. Also kommt auch in die MessageQueue erst die 2. Meldung vom Start, bevor einer das Beenden meldet.

Kannst dir doch mal die Uhrzeit/Tickcount anschauen, bei denen jeweils Anfang und Ende gemeldet werden.

e.f.q.

Aus Falschem folgt Beliebiges

3.170 Beiträge seit 2006
vor 17 Jahren

@Löwenherz
Wenn Du denvon Dir geposteten Artikel bei codeproject richtig gelesen hast, müsste Dein Problemeigentlich klar geworden sein. Ich halte den Artikel auch für sehr gut. Du scheinst an dieser Stelle noch ein grundsätzliches Verständnisproblem zu haben, lies den Artikel nochmal genau durch. Dann siehst Du, daß Du in DeinemFall entweder mit Invoke() arbeitenen solltest, oder Deine Threads eben manuell synchronisieren musst.

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

L
Löwenherz Themenstarter:in
58 Beiträge seit 2006
vor 17 Jahren

Joa, das kann gut sein. Ich habe eben noch ein bissl mit dem Problem experimentiert.
Folgender Code läuft bei mir wie gewünscht ab, und zwar so, dass die Print()-Methode nicht unterbrochen wird und sauber von Anfang bei Ende durchläuft, bevor sie eine neue Runde dreht und die Anforderung vom anderen Thread bedient. verzweifel


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

namespace BeginInvokeTest
{
  class Program : Form
  {
    private Thread thread1;
    private Thread thread2;

    Program()
    {
      Thread.CurrentThread.Name = "GUI Thread";
      thread1 = new Thread(new ThreadStart(ThreadProc1));
      thread2 = new Thread(new ThreadStart(ThreadProc2));
      thread1.Start();
      thread2.Start();
    }

    ~Program()
    {
      thread1.Join();
      thread2.Join();
    }

    private delegate void PrintDelegate(string s);

    private void ThreadProc1()
    {
      Thread.CurrentThread.Name = "ThreadProc1";
      while (true)
      {
        if (this.Created) this.BeginInvoke(new PrintDelegate(Print), new object[] { "Ich bin ThreadProc1" });
        Thread.Sleep(500);
      }
    }

    private void ThreadProc2()
    {
      Thread.CurrentThread.Name = "ThreadProc2";
      while (true)
      {
        if (this.Created) this.BeginInvoke(new PrintDelegate(Print), new object[] { "Ich bin ThreadProc2" });
        Thread.Sleep(500);
      }
    }

    void Print(string s)
    {
      Console.WriteLine(Thread.CurrentThread.Name + " [Anfang] - " + s);
      for (int i = 0; i < 10; i++) {
        Console.WriteLine(Thread.CurrentThread.Name + " - " + i);
        Thread.Sleep(0);
      }
      Console.WriteLine(Thread.CurrentThread.Name + " [Ende] - " + s);
    }


    static void Main(string[] args)
    {
      Program prog = new Program();
      Application.Run(prog);
    }
  }
}

Und das bildet eigentlich genau mein Problem nach, was ich mit Socket.BeginConnect und anschließendem Control.BeginInvoke zu haben scheine g Oh Mann 😉

L
Löwenherz Themenstarter:in
58 Beiträge seit 2006
vor 17 Jahren

Original von MarsStein
@Löwenherz
Wenn Du denvon Dir geposteten Artikel bei codeproject richtig gelesen hast, müsste Dein Problemeigentlich klar geworden sein. Ich halte den Artikel auch für sehr gut. Du scheinst an dieser Stelle noch ein grundsätzliches Verständnisproblem zu haben, lies den Artikel nochmal genau durch. Dann siehst Du, daß Du in DeinemFall entweder mit Invoke() arbeitenen solltest, oder Deine Threads eben manuell synchronisieren musst.

Sorry, ich habe nicht richtig geantwortet...
Ich habe den Artikel aufmerksam gelesen und der besagte genau das, was ich auch angenommen habe.

Mal eine Frage ... Was sollte BeginInvoke() denn sonst bringen, wenn nicht die Kontrolle an einen anderen Thread zu übergeben? Und da der GUI-Thread ja intern messagebasiert arbeitet, führt das mE zwangsweise zu einer Synchronisation.

L
Löwenherz Themenstarter:in
58 Beiträge seit 2006
vor 17 Jahren

Original von Traumzauberbaum
Du verstehst da glaub ich Synchronisation falsch.

Die Nachrichten werden in den GUI Thread auch genau in der Reihenfolge ausgewertet wie du sie reinsteckst. Nur hat das mit Synchronisation herzlich wenig zu tun.

Genau das passiert bei dir auch.

Synchronisation heißt grob gesagt, dass ein Thread auf einen anderen wartet.

Und genau das ist das Problem bei dir.

Das Geschehen läuft in 2 Threads ab, keiner wartet auf den anderen.
Der eine startet ohne auf das Beenden des Anderen zu warten. Also kommt der Aufruf zum Start des 2. vor dem beenden des 1. Also kommt auch in die MessageQueue erst die 2. Meldung vom Start, bevor einer das Beenden meldet.

Kannst dir doch mal die Uhrzeit/Tickcount anschauen, bei denen jeweils Anfang und Ende gemeldet werden.

Joa, das ist alles richtig, sehe ich auch so. Aber das betrifft eher 2 Threads, die "einfach so" (ohne Messagequeue) miteinander interagieren. Wenn ich eine Messagequeue habe, muss der aufrufende Thread doch eigentlich nur noch eine Message in die Queue stecken und die aufzurufende Methode wird dann ausgeführt, wenn der GUI-Thread die Message abholt und dann abarbeitet. Hmmm 😕

3.170 Beiträge seit 2006
vor 17 Jahren

Die MSDN besagt hierzu:

Control.Invoke-Methode
Führt einen Delegaten in dem Thread aus, der das dem Steuerelement zugrunde liegende Fensterhandle besitzt.

Control.BeginInvoke-Methode
Führt einen Delegaten asynchron für den Thread aus, in dem das dem Steuerelement zugrunde liegende Handle erstellt wurde.

Das heißt, auch mit Invoke() wird in Deinem Beispielcode immer der GUI-Thread arbeiten, nur eben dann synchron. Ich weiß jetzt auch wirklich gar nicht, was für Dich an der Geschichte eigentlich problematisch ist, sry 🤔

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

L
Löwenherz Themenstarter:in
58 Beiträge seit 2006
vor 17 Jahren

😁

Eigentlich gar nichts. Wir haben es offenbar nur geschafft aneinander vorbei zu diskutieren ^^

Und wie du schon sagst, es wird immer der GUI-Thread arbeiten. Der Witz ist aber, dass in meiner Applikation (scheinbar) im Kontext des GUI-Threads die aufgerufene Methode angefangen, unterbrochen, vom anderen Thread gestartet, wieder unterbrochen und dann zuende gelaufen ist. Das Problem ist bei mir der Wechsel des Kontextes während der Ausführung der Methode, obwohl ich BeginInvoke benutzt habe.

Die Grundlagen und wie BeginInvoke funktionieren sollte sind mir klar, aber das resultierende Verhalten (s.o.) hat mich echt fertig gemacht ^^ Ich glaube, ich schaue mir das morgen nochmal gaaaaaanz genau an. Kann ja nicht so schwer sein 😉 Ich habe sicherlich nur irgendwas übersehen.

Ich danke euch auf jeden Fall für die vielen Antworten! 😒hakehands:

L
Löwenherz Themenstarter:in
58 Beiträge seit 2006
vor 17 Jahren

Hallo nochmal und sorry, dass ich euch erneut behelligen muss 😉

Schaut euch mal bitte das folgende kleine Testprogramm an. Es reproduziert genau den Fehler, den ich meinte.


using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Threading;
using System.Diagnostics;

namespace TestApp
{
	/// <summary>
	/// Zusammenfassung für Form1.
	/// </summary>
	public class Form1 : System.Windows.Forms.Form
	{
		/// <summary>
		/// Erforderliche Designervariable.
		/// </summary>
		private System.ComponentModel.Container components = null;
    private Thread thread1;
    private Thread thread2;
    private Thread thread3;
    private System.Windows.Forms.Button m_ButtonStart;

		public Form1()
		{
			InitializeComponent();
      Thread.CurrentThread.Name = "GUI Thread";
		}

		/// <summary>
		/// Die verwendeten Ressourcen bereinigen.
		/// </summary>
		protected override void Dispose( bool disposing )
		{
			if( disposing )	{
				if (components != null) {
					components.Dispose();
				}
			}
			base.Dispose( disposing );
		}

		#region Vom Windows Form-Designer generierter Code
		/// <summary>
		/// Erforderliche Methode für die Designerunterstützung. 
		/// Der Inhalt der Methode darf nicht mit dem Code-Editor geändert werden.
		/// </summary>
		private void InitializeComponent()
		{
      this.m_ButtonStart = new System.Windows.Forms.Button();
      this.SuspendLayout();
      // 
      // m_ButtonStart
      // 
      this.m_ButtonStart.Location = new System.Drawing.Point(96, 112);
      this.m_ButtonStart.Name = "m_ButtonStart";
      this.m_ButtonStart.TabIndex = 0;
      this.m_ButtonStart.Text = "Start";
      this.m_ButtonStart.Click += new System.EventHandler(this.m_ButtonStart_Click);
      // 
      // Form1
      // 
      this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
      this.ClientSize = new System.Drawing.Size(292, 266);
      this.Controls.Add(this.m_ButtonStart);
      this.Name = "Form1";
      this.Text = "Form1";
      this.ResumeLayout(false);

    }
		#endregion

    private delegate void VoidDelegate();

    private void ThreadProc1()
    {
      Thread.CurrentThread.Name = "Thread 1";
      this.Invoke(new VoidDelegate(Print));
    }

    private void ThreadProc2()
    {
      Thread.CurrentThread.Name = "Thread 2";
      this.Invoke(new VoidDelegate(Print));
    }

    private void ThreadProc3()
    {
      Thread.CurrentThread.Name = "Thread 3";
      this.Invoke(new VoidDelegate(Print));
    }

    private void Print()
    {
      Debug.WriteLine("--- Print started: " + Thread.CurrentThread.Name + " ---");
      for(int i=0; i < 15; i++) {
        Debug.WriteLine("Line " + i);
        if (i == 10) MessageBox.Show("Es ist die 10. Runde!");
      }
      Debug.WriteLine("--- Print stopped: " + Thread.CurrentThread.Name + " ---");
    }

		/// <summary>
		/// Der Haupteinstiegspunkt für die Anwendung.
		/// </summary>
		[STAThread]
		static void Main() 
		{
			Application.Run(new Form1());
		}

    private void m_ButtonStart_Click(object sender, System.EventArgs e)
    {
      thread1 = new Thread(new ThreadStart(ThreadProc1));
      thread2 = new Thread(new ThreadStart(ThreadProc2));
      thread3 = new Thread(new ThreadStart(ThreadProc3));
      thread1.Start();
      thread2.Start();
      thread3.Start();
    }
	}
}

Im Log steht dann folgendes:


--- Print started: GUI Thread ---
Line 0
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
--- Print started: GUI Thread ---
Line 0
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
--- Print started: GUI Thread ---
Line 0
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
Line 11
Line 12
Line 13
Line 14
--- Print stopped: GUI Thread ---
Der Thread 'Thread 2' (0xa98) hat mit Code 0 (0x0) geendet.
Line 11
Line 12
Line 13
Line 14
--- Print stopped: GUI Thread ---
Der Thread 'Thread 3' (0xbf4) hat mit Code 0 (0x0) geendet.
Line 11
Line 12
Line 13
Line 14
--- Print stopped: GUI Thread ---
Der Thread 'Thread 1' (0x29c) hat mit Code 0 (0x0) geendet.

Noch während ich mich mit meinem GUI-Thread in der Ausführung von Print() befinde, wechselt die Kontrolle und der GUI-Thread beginnt die Print()-Methode erneut. Wie kann das denn sein, wenn Control.Invoke() und Control.BeginInvoke() angeblich nichts weiter tun, als eine Message in die Messagequeue des Steuerelements einzuhängen?!

Ich werde daraus echt nicht schlau, sorry 😦

Aber vielleicht kann mir ja eine(r) von euch sagen, wo ich da den Knoten im Denkapparat habe 😁

3.170 Beiträge seit 2006
vor 17 Jahren

Hm. Schau nochmal in den von Dir geposteten Artikel im Codeproject.
Da steht:

The major difference is that SendMessage blocks the caller till the message gets processed by the message pump whereas PostMessage returns immediately.

und

Invoke: This is like the SendMessage API function in that it waits till the message gets processed, but it does not do a SendMessage internally, it also does a PostMessage. The difference is that it waits till the delegate is executed on the UI thread before returning.

Das bedeutet dann wohl, daß wohl Dein eigener rufender Thread bei Invoke() wartet, bis die Message verarbeitet ist, bei BeginInvoke() braucht er nicht zu warten.
Das hat aber nichts damit zu tun, daß der GUI-Thread, der die Messages ja verarbeitet, dadurch blockiert würde. Er nimmt die nächste Message von der Queue und fängt an, diese zu verarbeiten, weil es ja eine PostMessage war.
Die Tatsache, daß alle Print()-Aufrufe vom GUI Thread abgearbeitet werden, heißt eben nicht, daß sie auf diesem synchron abgearbeitet werden!! Dafür mußt Du dann selber sorgen.

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

L
Löwenherz Themenstarter:in
58 Beiträge seit 2006
vor 17 Jahren

Hmm, ok 🙂

Ich glaube, dann habe ich es verstanden - glaube ich. Danke dir!

Auch wenn in den CodeProject-Artikel steht, dass es eine Messageloop intern gibt, die die PostMessage-Messages abarbeitet. Und solange der Handler einer dieser Messages noch nicht vollständig durchgelaufen ist, sollte die nächste warten müssen. Daher kam so das Problem auf ^^

L
Löwenherz Themenstarter:in
58 Beiträge seit 2006
vor 17 Jahren

Achso, mir fällt da noch was zur Ergänzung ein...

Ersetze doch mal den MessageBox.Show()-Aufruf durch einen Aufruf von Form.ShowDialog(). Das gibt das gleiche Problem. Der Witz ist irgendwie die Implementierung von modalen Dialogfenstern in .NET.

Wenn du die besagte Zeile aber durch ein Thread.Sleep() oder ein AutoResetEvent (z.B. m_event.WaitOnE(1000, false)) ersetzt. Dann blockiert der GUI-Thread wirklich und es ist alles so wie ich es erwarten würde. Nur diese bekloppten modalen Forms machen da echt Ärger... warum auch immer 😉

Herbivore hat in einem anderen Thread hier auch schonmal darauf hingewiesen, dass MessageBoxes beim Debuggen das allerletzte sind. Ich glaube, ich weiß jetzt auch, wieso 😉

3.170 Beiträge seit 2006
vor 17 Jahren

Ja ohne den modelen Dialog wird deer GUI-Thread überhaupt nicht unterbrocheen und arbeitet dann erst mal das ab wo er dran ist. Modale Dialoge können da schon seltsame Effekte erzeugen.
Gut das alles mal durchgekaut zu haben, denn oft braucht man ja modale Dialoge oder MessageBoxen, die nicht zum Debuggen gedacht sind, und dann weiß man wo man dran ist.

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

S
8.746 Beiträge seit 2005
vor 17 Jahren

Eigentlich ist das Verhalten gar nicht seltsam. Zudem willst du ja auch nicht, dass Ereignisse verloren gehen, während ein modaler Dialog den Programmablauf unterbricht.

L
Löwenherz Themenstarter:in
58 Beiträge seit 2006
vor 17 Jahren

Hm, tun die Ereignisse (Messages) ja auch nicht, die werden halt gequeued und später abgearbeitet. Irgendwie ist das ein toller Fall mit Laufzeitproblemen, für den man nur einen Thread haben muss 😁