Laden...

Abgeborchener Thread soll nichts mehr schreiben

Erstellt von Zobel vor 14 Jahren Letzter Beitrag vor 14 Jahren 3.545 Views
Z
Zobel Themenstarter:in
5 Beiträge seit 2009
vor 14 Jahren
Abgeborchener Thread soll nichts mehr schreiben

Hallo! Ich bin Anfänger, was PC-Programmierung angeht.

Nachfolgendes Programm ist nur zur Übung für mich, und nicht unbedingt sinnvoll.

Beim Drücken vom Start-Button wird ein Thread erstellt, der einen Host anpingt. Der Thread schreibt die Ergebnisse zyklisch per BeginInvoke im GUI-Thread in ein mehrzeiliges Textfeld.

Wird der GUI-Button erneut gedrückt, dann soll das "Pingen" beendet werden.
Allerdings gib es zwei Probleme:

  1. Der Thread wird per Thread.Abort() abgebrochen, allerdings schreibt der Thread trotzdem noch was in das Ausgabefeld. Wie kann ich das sicher verhindern.

(2. Wenn der Thread gerade die Ping.Send(...)-Methode aufruft, welche blockiert, dann gibt es eine PingException. Eignetlich so doch laut Doku eine ThreadAbortException kommen.)

PS: Wie kann man eigentlich hier die Nummernzeile vor dem Code anzeigen?

Danke für die Hilfe!


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.Net;
using System.Net.NetworkInformation;

namespace PingTester
{
public partial class Form1 : Form
{
private Thread PingThread;
private string hostNameOrAddress;

// This delegate enables asynchronous calls for setting
// the text property on a TextBox control.
delegate void AppendDebugTextCallback(string text);


public Form1()
{
InitializeComponent();
this.AppendDebugText("Application started\r\n";);
}

private void btnStartStopp_Click(object sender, EventArgs e)
{
if (this.btnStartStopp.Text == "Start";)
{
this.hostNameOrAddress = txbHostNameOrAddress.Text;
if (hostNameOrAddress.Length != 0)
{
this.btnStartStopp.Text = "Stop";
this.AppendDebugText("Ping started: " + hostNameOrAddress + "\r\n";);
this.PingThread = new Thread(new ParameterizedThreadStart(this.runPingThread));
this.PingThread.IsBackground = true; // thread stops, when application is closing
this.PingThread.Start(hostNameOrAddress);
}
}
else
{
this.PingThread.Abort();
// this.PingThread.Join(); // hilft nicht
this.AppendDebugText("Ping stopped \r\n";);
this.btnStartStopp.Text = "Start";
}
}

private void runPingThread(object TargetString)
{
try
{
Ping pingSender = new Ping();
PingOptions options = new PingOptions();

// Create a buffer of 32 bytes of data to be transmitted.
string data = "abcdefghijklmnopqrstuvwxyzabcdef";
byte[] buffer = Encoding.ASCII.GetBytes(data);
int timeout = 5000;
while (true)
{
PingReply reply = pingSender.Send((string)TargetString, timeout, buffer, options);
if (reply.Status == IPStatus.Success)
{
AppendDebugText("Reply from: " + reply.Address.ToString()
+ ": Time=" + reply.RoundtripTime.ToString() + "\r\n";);
// Thread.Sleep(1000);
}
else
{
AppendDebugText(reply.Status.ToString() + "\r\n";);
}
}
}
catch (ThreadAbortException)
{
}
catch (PingException e)
{
AppendDebugText(e.Message + "\r\n";); // Waehrend einer Pinganforderung ist eine Ausnahme aufgetreten.
AppendDebugText(e.InnerException.Message + "\r\n";); // Der Thread wurde abgebrochen.
}
finally
{
}
}

// This method demonstrates a pattern for making thread-safe
// calls on a Windows Forms control.
//
// If the calling thread is different from the thread that
// created the TextBox control, this method creates a
// AppendDebugTextCallback and calls itself asynchronously using the
// Invoke method.
//
// If the calling thread is the same as the thread that created
// the TextBox control, the DebugText is append directly.
private void AppendDebugText(string debugText)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.txbHostNameOrAddress.InvokeRequired)
{
AppendDebugTextCallback d = new AppendDebugTextCallback(AppendDebugText);
this.BeginInvoke(d, new object[] { debugText }); // BeginInvoke is asynchronous
}
else
{
this.txbDebugOut.AppendText(debugText);
}
}

private void txbTarget_TextChanged(object sender, EventArgs e)
{

}
}
}

J
237 Beiträge seit 2008
vor 14 Jahren

Du könntest einen BackgroundWorker nehmen.
Statt while(true) dann while(!bgw.CancelationPending). Dann kannst du beim drücken des Buttons bgw.CancelAsync() aufrufen und schreiben "Wird beendet..." und im Ereignis bgw_RunWorkerCompleted dann schreiben "Pingen beendet.". So hast du nicht das Problem der PingException und kannst nebenbei die BeginInvoke-Aufrufe sparen.
Siehe: MSDN: Der BackgroundWorker

Grüße, JasonDelife.

Beim Programmieren löst man die Probleme, die man nicht hätte, programmierte man nicht.

5.742 Beiträge seit 2007
vor 14 Jahren

Du könntest einen BackgroundWorker nehmen.
Statt while(true) dann while(!bgw.CancelationPending).

Das geht auch ohne BGW: Einfach eine Variable volatile bool cancel (nur zur Sicherheit) nehmen, und im Thread prüfen, ob diese true ist. (und natürlich auch entsprechend setzten).

Wie kann man eigentlich hier die Nummernzeile vor dem Code anzeigen?

Sollte irgendwo in den VS Optionen zu "TextEditor" möglich sein.

49.485 Beiträge seit 2005
vor 14 Jahren

Hallo Zobel,

... und das solltest du auch so machen, denn Thread.Abort ist böse.

herbivore

Z
Zobel Themenstarter:in
5 Beiträge seit 2009
vor 14 Jahren

Vielen Dank für die Hilfe!

Ich werde mir das mal mit dem BackroundWorker anschauen, dauert allerdings ein wenig, da ich nur in meiner Freizeit dazu komme. =)

Warum ich eigentlich den Thread sofort beenden möchte:

Wenn jemand den Button "Stop" drückt, soll er danach gleich mit dem Button "Start" ein neues Ziel anpingen können.

Breche ich den Worker Thread mit Abort() ab, oder fordere, wie vorgeschlagen, den Worker Thread per Flag "volatile bool cancel" zum Beenden auf, habe ich das Problem, dass im Worker Thread die Ping.Send()-Methode bis zu 5 Sekunden blockiert (vorgegebener Timeout im Bsp.) und das Beenden erst danach ausgeführt wird.

Ich könnte zwar einfach ignorieren, dass der alte Worker Thread noch nicht wirklich beendet ist und im GUI-Thread sofort zurückkehren. Drückt dann jemand sofort danach auf "Start" würde ich einen neuen Worker Thread für das neue Ping-Ziel erstellen. Problem dabei: wacht dann irgendwann der alte Worker Thread auf, schreibt er mir eventuell noch unerwünschten Text in die Ausgabe.

Es würde also reichen, wenn ich verhindere, dass der alte Worker Thread noch Text in die Ausgabe schreiben kann. Wäre das irgendwie möglich?
Oder ist mein Herangehen/Desing vielleicht völlig verkehrt?

mfg

PS: Ich habe zwar gefunden, wo ich in Visual Studio die Zeilennummern anzeigen kann. Allerdings wüsste ich nicht, wie ich die Zeilennummern hier ins Forum. bekomme. Wie kann ich hier Quellcode mit Zeilennummern posten?

U
1.688 Beiträge seit 2007
vor 14 Jahren

Problem dabei: wacht dann irgendwann der alte Worker Thread auf, schreibt er mir eventuell noch unerwünschten Text in die Ausgabe.

Du musst natürlich nach der (lang andauernden) Ping-Funktion direkt das "cancel"-Flag prüfen und ggf. den Thread damit beenden.

3.971 Beiträge seit 2006
vor 14 Jahren

Richtig wäre, wenn du statt Ping.Send die asynchrone Funktion Ping.SendAsync verwenden würdest, dann bräuchtest du kein Thread erstellen.

Ich könnte zwar einfach ignorieren, dass der alte Worker Thread noch nicht wirklich beendet ist und im GUI-Thread sofort zurückkehren. Drückt dann jemand sofort danach auf "Start" würde ich einen neuen Worker Thread für das neue Ping-Ziel erstellen.

Wenn du die asynchrone Methode nicht einsetzen willst, ist das der richtige Weg.

Du musst natürlich nach der (lang andauernden) Ping-Funktion direkt das "cancel"-Flag prüfen und ggf. den Thread damit beenden.

Nein, das funktioniert auch nicht, wenn er in der Zeit bereits einen neuen Thread am laufen hat. Backgroundworker würde in dem Fall auch nicht helfen.

Was dein Problem löst wäre eine seperate Klasse ("Pinger"-Klasse), die den Ping (wahlweise in einem neu erstellten Thread) ausführt, ein eigenes Cancel-Property besitzt und bei der async-Methode einen eigenen PingCompleted-EventHandler besitzt. Deine Gui merkt sich nur die aktuelle Instanz. Wird ein neuer Ping gestartet, wird Cancel auf deiner aktuellen "Pinger"-Instanz ausgeführt und im GUI ein neue "Pinger"-Klasse erstellt.

Es gibt 3 Arten von Menschen, die die bis 3 zählen können und die, die es nicht können...

Z
Zobel Themenstarter:in
5 Beiträge seit 2009
vor 14 Jahren

Leider habe ich das nicht ganz verstanden, kannst Du das bitte etwas näher beschreiben?

Was dein Problem löst wäre eine seperate Klasse ("Pinger"-Klasse), die den Ping (wahlweise in einem neu erstellten Thread) ausführt, ein eigenes Cancel-Property besitzt und bei der async-Methode einen eigenen PingCompleted-EventHandler besitzt. Deine Gui merkt sich nur die aktuelle Instanz. Wird ein neuer Ping gestartet, wird Cancel auf deiner aktuellen "Pinger"-Instanz ausgeführt und im GUI ein neue "Pinger"-Klasse erstellt.

Ich habe mal eine volatile int counter - Variable eingefügt und damit untersucht, wann wer die Nachrichten schreibt. So wie es aussieht, ruft der WorkerThread die asynchrone Methode

private void AppendDebugText(string debugText)

auf, bevor er mit Thread.Abort() beendet werden sollte. Kann also noch gar nicht wissen, dass er nicht mehr schreiben dürfte.
Die Nachrichten werden aber erst in den GUI eingefügt, wenn der GUI-Thread durchgelaufen ist. Schreibt der GUI-Thread aber in diesem Durchlauf auch mit dieser Methode

private void AppendDebugText(string debugText)

, dann kommt bei mir die Reihenfolge durcheinander.

Ist wohl doch etwas zu kompliziert für mich, vielleicht sollte ich mir für den Anfang =)etwas einfacheres suchen.

49.485 Beiträge seit 2005
vor 14 Jahren

Hallo Zobel,

Ist wohl doch etwas zu kompliziert für mich, vielleicht sollte ich mir für den Anfang fröhlich etwas einfacheres suchen.

es ist grundsätzlich empfehlenswert, sich nicht zu schnell an zu komplizierte Sachen zu wagen. Und Threading hat es durchaus in sich. Das ist es nicht schlecht, wenn man entsprechend Sattelfest ist, bevor man damit anfängt.

herbivore

742 Beiträge seit 2005
vor 14 Jahren

Du könntest auch einfach den Thread in Ruhe beenden und verhindern, dass er keine Ausgabe mehr macht und dann einen neuen Thread für die neue Aktion erstellen, z.B. einfach mit dem ThreadPool.

Z
Zobel Themenstarter:in
5 Beiträge seit 2009
vor 14 Jahren

Du könntest auch einfach den Thread in Ruhe beenden und verhindern, dass er keine Ausgabe mehr macht und dann einen neuen Thread für die neue Aktion erstellen, z.B. einfach mit dem ThreadPool.

Das gemeine ist, dass Ping.Send() mit einem Timeout aufgerufen wurde. Falls zum Bsp. ein nicht erreichbares Ziel angepingt wird, dann blockiert diese Methode für die angegebene Zeit und läßt sich auch nicht durch Thread.Abort() zum vorzeitigen abbrechen bewegen. Nach Deiner Vorgabe könnte man dann für diese Zeit keinen neuen Ping starten, was unschön ist.

Habe aber gerade gesehen, dass es auch ein nicht blockierendes Ping gibt. Vielleicht sollte ich es mal damit probieren und dann, wie von Dir und oben beschrieben, den WorkerThread in Ruhe sich beenden lassen.

Wenn das nicht geht, dann lasse ich es eben erstmal. Ist ja nur ne Übungsaufgabe für mich gewesen.

Allen nochmal vielen Dank!

3.971 Beiträge seit 2006
vor 14 Jahren
Abgeborchener Thread soll nichts mehr schreiben

Hallo Zobel,
so könnte das ganze in etwa aussehen: (Code nicht getestet!)


public class Pinger {

private string m_host;
private Ping m_ping;

public System.Windows.Forms.ListBox ListBox { get; set; }

public bool Cancel { get; set; }

public void Ping(string host) {
  m_ping = new Ping();
  m_host = host;
  Ping();
}

private void Ping() {
  m_ping.PingAsync(m_host, Ping_Completed);
}

private void Ping_Completed(object ignored, PingCompletedEventArgs e) {
  if (!Cancel) {
    ListBox.Invoke(new MethodInvoker() {
      ListBox.Items.Add(...);
    }
    Ping();
  }
}
}

Deine GUI könnte dann beispielsweise so aussehen:


public MyForm : Form {
  private Pinger m_currentPinger;

  void StartButton_Click(object sender, EventArgs e) {
    if (m_currentPinger != null) m_currentPinger.Cancel = true;
    m_currentPinger = new Pinger();
    m_currentPinger.ListBox = this.ResultBox;
    m_currentPinger.Ping(this.HostTextBox.Text);
  }
}

PS: Wenn du kein PingOfDeath erzeugen willst, sollte die private Methode Pinger.Ping() von einem System.Threading.Timer aus gestartet werden.

Es gibt 3 Arten von Menschen, die die bis 3 zählen können und die, die es nicht können...

Z
Zobel Themenstarter:in
5 Beiträge seit 2009
vor 14 Jahren

Hallo kleines_eichhoernchen! Danke für den Bsp.-Code

Ich gabe das mal probiert. Die Klasse sieht jetzt bei mir so aus:


class Pinger
    {
        private string host;
        private Ping pingSender;

        public System.Windows.Forms.TextBox DbgTextBox { get; set; }

        public bool Cancel { get; set; }

        private delegate void addListItem(String myString);
        private addListItem myDelegate;

        public void Ping(string host)
        {
            pingSender = new Ping();
            pingSender.PingCompleted += new PingCompletedEventHandler(pingCompletedCallback);
            this.host = host;
            myDelegate = new addListItem(addListItemMethod);
            ping();
        }

        private void ping()
        {
            pingSender.SendAsync(host, new Object());
        }

        private void pingCompletedCallback(object sender, PingCompletedEventArgs e)
        {
            if (!Cancel)
            {
                DbgTextBox.AppendText("Test1\r\n");
                DbgTextBox.Invoke(myDelegate, new Object[] {"Test2\r\n"});
                DbgTextBox.AppendText(Thread.CurrentThread.Name + "\r\n");
                // Thread.Sleep(3000)
                ping();
            }
        }

        private void addListItemMethod(String myString)
        {
            DbgTextBox.AppendText(myString);
        }

    }

So wie es aussieht, wird "pingCompletedCallback" im GUI-Thread ausgeführt.

  1. Ich habe mal Thread-Namen im Konstruktur von Form1 aum MainThread gesetzt und in "pingCompletedCallback" ausgelesen.
  2. Und falls ich Thread.Sleep aktiviere, dann blockiert die GUI für die angegebene Zeit.

Benötigt man dann überhaupt noch .Invoke()???

Jetzt habe ich allerdings noch ein anderes Problem. Wenn ich das Programm schliesse, während der Ping aktiv ist. Wird am Ende von "Application.Run(new Form1());" die folgende Exception gemeldet:

System.Reflection.TargetInvocationException
InnerException: System.ObjectDisposedException

Ich nehme mal an, das kommt daher, dass der GUI-Thread beendet wurde, der Ping im Hintergrund aber noch zu Ende läuft, dann die "pingCompletedCallback" aufruft, aber die TextBox nicht mehr da ist. Irgendwie überfordert mich das. Habs mir ehrlich nicht so kompliziert vorgestellt. 🙁

49.485 Beiträge seit 2005
vor 14 Jahren

Hallo Zobel,

Ich nehme mal an, das kommt daher, dass der GUI-Thread beendet wurde, der Ping im Hintergrund aber noch zu Ende läuft, dann die "pingCompletedCallback" aufruft, aber die TextBox nicht mehr da ist.

richtig, sowas muss man halt synchronisieren. Multithreading ist durchaus nicht trivial. Du hattest ja oben selbst überlegt, ob es besser ist, das erstmal zurückzustellen und ich hatte das bestätigt. An Multithreading solltest du dich erst machen, wenn die sonstigen Grundlagen sitzen und durch dich außerdem mit der Theorie von Multithreading und Synchronisation beschäftigt hast.

herbivore

3.971 Beiträge seit 2006
vor 14 Jahren

In pingCompletedCallback musst du Invoke verwenden, da der Callback-Thread irgendeiner aus dem ThreadPool von .NET ist. Und da Callback-Thread und GUI-Thread nicht der selbe ist, musst du Invoke verwenden.

Um die Disposed-Exception abzufangen, musst du noch prüfen, ob das Handle der Listbox noch gültig ist.

  
pingSender.SendAsync(host, new Object());  
  

Wenn du das State-Object wie in deinem Fall nicht brauchst, übergebe einfach null. Entlastet den GC

Weiterhin sollte man Thread.Sleep nicht verwenden. Wenn du eine Funktion periodisch ausführen möchtest, solltest du einen Timer benutzen. In deinem Fall bietet sich System.Threading.Timer an.


class Pinger {
  private Timer m_tmr;

  public bool Cancel { get; set; } //Hier Timer Disposen

  public void Ping(string host) {
    pingSender = new Ping();
    pingSender.PingCompleted += new PingCompletedEventHandler(pingCompletedCallback);
    this.host = host;
    myDelegate = new addListItem(addListItemMethod);

    m_tmr = new Timer(x => {  //Stichwort Lambda-Ausdruck und anonyme Delegaten
      Ping();
    }, null, 0, 3000);
  }

  //In pingCompletedCallback nicht Ping() aufrufen
}

Allerding solltest du aufpassen, dass sich die perioden-Werte von dem Timer und der Pinger-Klasse nicht überschneiden, könnte zu lustigen effektiven führen (Stichwort Threading-Synchronisation)

Es gibt 3 Arten von Menschen, die die bis 3 zählen können und die, die es nicht können...