Laden...

lesender Thread (SerialPort) führt bei Join() zum Blockieren der GUI

Erstellt von #CPferdchen vor 12 Jahren Letzter Beitrag vor 12 Jahren 3.581 Views
Thema geschlossen
#
#CPferdchen Themenstarter:in
28 Beiträge seit 2011
vor 12 Jahren
lesender Thread (SerialPort) führt bei Join() zum Blockieren der GUI

Ahoi,

ich habe, um den Umgang mit Seriellen Schnittstelllen zu lernen, ein kleines Beispielprogramm gebastellt. Eine Forms-Anwendung mit einem Button zur Initialisierung und Öffnen eines neuen seriellen Ports. Einen weiteren der eine Methode als Thread aufruft, welche den Port ausließt und einen, den Problembutton, der die serielle Anbindung schießt, den Thread erst mit Join stoppen soll und dann mit Abort beenden.

Der Teilnehmer am Port ist eine USB GPS Reciver der NMEA Signale ausgibt.
Alle die serielle Schnittstelle betreffenden Funktionnen sind in einer gesonderten Klasse untergebracht. Die Klasse enthält die Methode DoWork(), welche durch einen Thread im Form-Thread aufgerufen wird. Desweiteren zwei Methode um eine Endlossschleife in DoWork von aussen verlassen und betretten zu können. Also so wie man es unzähligen Beispielen findet...

Im Folgenden die oben beschriebene Klasse wobei ich die Funktionalität zum Öffnen und Lesen jetz mal weg gelassen habe:


using System.Threading;
using System.IO.Ports;

namespace GPS__Test_0._1
{
    class GPS
    {
        Form1 f;
        SerialPort serialPort;

        private volatile bool _shouldStop = false;


         public GPS(Form1 f)
        {
            this.f = f;
        }

        public void Unint()
        {
            try
            {
                if (serialPort.IsOpen)
                {
                    serialPort.Close();
                }
            }
            catch (Exception ex)
            {
                f.SetText("Unint Exception: " + ex);
            }
        }

        public void DoWork()
        {
            try
            {
                while (true)
                {
                    if (_shouldStop)
                        break;
                    
                    string msg = serialPort.ReadLine();

                    f.SetText(msg);

                    Thread.Sleep(100);

                   
                }
            }
            catch (StackOverflowException SOex)
            {
                f.SetText("DoWork Exception: " + SOex);
            }
            catch(Exception ex)
            {
                f.SetText("DoWork Exception: " + ex);
            }
        }

        public void RequestStop()
        {
            _shouldStop = true;
        }

        public void RequestStart()
        {
            _shouldStop = false;
        }
}

Meine Klasse Form


namespace GPS__Test_0._1
{
    public partial class Form1 : Form
    {
        GPS gps;
        Thread gpsThread;

        public Form1()
        {
            InitializeComponent();
            gps = new GPS(this);
        }

        delegate void SetTextCallback(string text);

        public void SetText(string text)
        {
            if (lbxStatus.InvokeRequired)
            {
                SetTextCallback d = new SetTextCallback(SetText);
                this.Invoke(d, new object[] { text });
            }
            else
            {
                this.lbxStatus.Items.Add(text);

                lbxStatus.SelectedIndex = lbxStatus.Items.Count - 1;
            }
        }

         private void btnInit_Click(object sender, EventArgs e)
        {
            gps.RequestStart();
            SetText("Request started");
            gps.Init("COM6");
            SetText("Init with COM6 called");
        }

        private void btnStart_Click(object sender, EventArgs e)
        {
            try
            {
                gps.RequestStart();
                gpsThread = new Thread(gps.DoWork);
                gpsThread.Start();
            }
            catch (Exception ex)
            {
                SetText("GPS Thread starting: " + ex);
            }
        }

        private void btnStop_Click(object sender, EventArgs e)
        {
            
            try
            {
                gps.RequestStop();
                gpsThread.Join();
                gpsThread.Abort();
                gps.Unint();
            }
            catch (Exception ex)
            {
                SetText("GPS Thread stopping: " + ex);
            }
            lbxStatus.Items.Add("gpsThread stopped");
        }
    }
}

Wenn ich mich nun in der Situation befinde, dass meine Port geöffnet ist, die NMEA Signale in meiner ListBox ausgegeben werde und ich den Stop Button drücke gefriert mein Fenster. Debuggen hat gezeigt es liegt an

gpsThread.Join();

Warum?

1.
Hat es mit der Form1.SetText() Methode zu tun? Ist das so überhaupt Threadsafe oder überhaupt sauber Objektorientirt? Das der Absturz daher rüht, dass noch Nachrichten auf der ListBox mit SetText ausgeben werden sollen, aber das irgendwie nicht geht und so das gpsThread ewig auf das FormThread wartet?

oder 2.
In der MSDN Lib wird Join mit
"Blockiert den aufrufenden Thread, bis ein Thread beendet wird, während das Standard-COM- und das SendMessage-Pumping fortgesetzt werden. " beschrieben.
Hat dann damit zu tun, dass der Thread nicht gestoppt werden kann, weil noch Nachrichten am COM Port ankommen. Und wenn ja, warum, da ich den Port eigentlich schon geschlossen habe, wenn ich Join ausführe.

Ich hoffe ihr könnt einem Neuling weiter helfen.

Mit freundlichen Grüßen,
Cpferdchen

F
10.010 Beiträge seit 2004
vor 12 Jahren

Das Problem kommt aus verschiedenen Gründen zu Stande.

  1. ReadLine wartet bis Timeout oder NewLine ankommt.
  2. Weil das blockierend ist, blockiert auch das Join solange bis readline zurückkommt.

Lösungen

  1. Warum benutzt du nicht die Events des SerialPort ( DataReceived )?
  2. Bitte schau dir dringend die Pattern von .NET an, deine UnInit würde man eher als Dispose() ausführen und damit auch das IDisposable Interface implementieren.
U
1.688 Beiträge seit 2007
vor 12 Jahren
  1. ReadLine wartet bis Timeout oder NewLine ankommt.
  2. Weil das blockierend ist, blockiert auch das Join solange bis readline zurückkommt.
  1. Wenn Readline denn zurückkehren würde (falls Daten ankommen oder Du ein ReadTimeout gesetzt hättest), würde das Programm trotzdem blockieren, da
    3a) Dein GUI-Thread im Button-Klick hängt (nämlich wg. Join) und
    3b) f.SetText somit nicht per Invoke in den GUI-Thread wechseln kann.

Hier könnte man Invoke durch BeginInvoke ersetzen.

Wobei der Aufbau eigentlich noch anders sein sollte: man übergibt nicht die Form, sondern bietet Events an. Warum und wie steht schon sehr oft hier im Forum.

Außerdem ist es prinzipiell keine gute Idee im GUI-Thread zu warten (Join). Auch dazu findet sich massenhaft Material im Forum.

6.911 Beiträge seit 2009
vor 12 Jahren

Hallo #CPferdchen,

Auch dazu findet sich massenhaft Material im Forum.

So ist es. Siehe [FAQ] Warum blockiert mein GUI? und [FAQ] Eigenen Event definieren / Information zu Events (Ereignis/Ereignisse).

Invoke durch BeginInvoke ersetzen

Hierzu siehe auch [FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke), dort ist der Unterschied zwischen Invoke und BeginInvoke beschrieben.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

888 Beiträge seit 2007
vor 12 Jahren

Und hier noch Hilfe zum Thema SerialPort:

Template SerialPort

Hinweis von herbivore vor 12 Jahren

Das ist auch nur eine Variante des schon genannten [FAQ] Warum blockiert mein GUI? Insofern [Hinweis] Wie poste ich richtig? Punkt 1.1.1.

Thema geschlossen