Entwickler-Ecke

Basistechnologien - was ist die beste vorgehensweise zu Exceptions in Threads?


funcry - So 21.06.09 10:32
Titel: was ist die beste vorgehensweise zu Exceptions in Threads?
Ich nähere mich gerade dem Thema Exceptions, und habe festgestellt, dass es bei Threads Besonderheiten gibt. Nach der Durchsicht mancher MSDN Dokumente muss ich feststellen, daß ich dabei nicht alles verstanden habe.

Mein Problem ist, ich habe eine nicht-GUI Klasse welche Threaded ausgeführt wird. Dabei kann es durchaus zu Exceptions kommen. Durch Test habe ich herausgefunden, daß diese nicht aus dem jeweiligen Thread herausgeleitet werden. Meine Überlegung ist, daß die außerste aufrufende Klasse, welche immer eine GUI-Klasse ist, die Exception kennen sollte, diese dem User anzeigen sollte, und hier die Entscheidung getroffen werden sollte, wie die Anwendung fortfahren kann.

Beispiel:
Es geht um das Einlesen und Interpretieren(!) von CSV-Dateien. Möglicherweise hat der Anwender einen falschen CSV-Seperator eingegeben, dies führt zu einer Exception in der einlesenden nicht GUI-Klasse.

Quelltext-Beispiel:
Ich habe zur Veranschaulichung zwei try catch-Blöcke eingebaut. Dabei ist bei einem Fehler im Konstruktor von Samples folgendes zu beobachten:
Tritt im Konstruktor von Samples eine Exception auf, wird diese im inneren catch-Block angezeigt, jedoch nicht an den außeren Catch-Block weitergeleitet. Wie gesagt, ich könnte Events definieren (z.B. ReadErrorOccurred, ConvertErrorOccurred) und damit die Exceptions nach außen führen - aber ist das der richtige Weg ?


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
public void CreateSamplesAsync(CsvAggregator csvAggregator, SettingsTeach settingsTeach, string guidStr)
        {
            OnSampleCreationBegin(EventArgs.Empty);

            int threadCount = csvAggregator.SelectedCsvCores.Count;
            if (threadCount > 0)
            {
                for (int i = 0; i < csvAggregator.SelectedCsvCores.Count; i++)
                {
                    CsvCore csvCore = csvAggregator.SelectedCsvCores[i];

                    try
                    {
                        ThreadPool.QueueUserWorkItem(ignore =>
                        {
                            try
                            {
                                OnSampleCreationBeginThread(csvCore);

                                for (int j = 0; j < settingsTeach.ParallelInstances; j++)
                                {
                                    DBAccess.SaveSamples(csvCore.Symbol, 0, j, guidStr, new Samples(settingsTeach.DateTeachFrom, settingsTeach.DateTeachTo, false, csvCore, settingsTeach.PrevDaysCount, settingsTeach.ForecastDaysCount));  // in dieser Zeile wird die Exception ausgelöst.
                                    DBAccess.SaveSamples(csvCore.Symbol, 1, j, guidStr, new Samples(settingsTeach.DateCheckFrom, settingsTeach.DateCheckTo, true, csvCore, settingsTeach.PrevDaysCount, settingsTeach.ForecastDaysCount));
                                }

                                OnSampleCreatedThread(csvCore);

                                if (Interlocked.Decrement(ref threadCount) == 0) OnSampleCreationCompleted();
                            }
                            catch
                            {
                                throw;
                            }
                        });
                    }
                    catch
                    {
                        // Hier lande ich nie, auch dann nicht, wenn ich den inneren try-catch block entferne.
                        throw;
                    }
                }
            }
            else OnSampleCreationCompleted();
        }


gfoidl - So 21.06.09 11:07

Hallo,

um Exceptions in Threads an den Aufrufer weiterzuleiten kann wie in folgendem Beispiel vorgegangen werden. Ich verwende meist diesen Weg obwohl es noch eine andere Möglichkeit gibt -> siehe unten.

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
using System;
using System.Threading;

namespace Thread_und_Exception
{
  class Program
  {
    private static readonly object _locker = new object();
    private static Exception _threadException;
    private static AutoResetEvent _signal = new AutoResetEvent(false);
    //---------------------------------------------------------------------
    static void Main(string[] args)
    {
      Console.WriteLine("Thread starten...");

      try
      {
        // Thread starten:
        ThreadPool.QueueUserWorkItem(MacheFehler);
        _signal.WaitOne();

        // Prüfen ob ein Fehler aufgetreten ist:
        if (_threadException != null)
          throw _threadException;
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex);
      }

      Console.WriteLine("Warten bis Thread fertig ist.");
      Console.WriteLine("Ende.");
      Console.ReadKey();
    }
    //---------------------------------------------------------------------
    public static void MacheFehler(object o)
    {
      try
      {
        Console.WriteLine("Im Thread.");

        // Fehler auslösen:
        throw new Exception("Fehler im Thread.");

        Console.WriteLine("Thread Ende.");
      }
      catch (Exception ex)
      {
        // Threadsicherheit -> es könnten ja zwei Threads gleichzeit
        // darauf zugreifen wollen:
        lock (_locker)
        {
          _threadException = new Exception(ex.Message, ex);
        }        
      }

      // Benachrichtigen dass Thread fertig ist:
      _signal.Set();
    }
  }
}

Werden mehrere Threads verwendet kann für "_threadException" aus obigen Beispiel eine List<Exception> verwendet werden der die Fehler hinzugefügt werden.

Eine andere Möglichkeit existiert bei WinForm-Projekten bzw. wenn auf System.Windows.Forms.dll verwiesen wird.
Die Application-Klasse bietet ein ThreadException-Ereignis.

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
[STAThread]
static void Main()
{
  Application.ThreadException +=
    (sender, e) => MessageBox.Show(e.Exception.ToString());

  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
  Application.Run(new Form1());
}

Dies sollte aber nur als Notlösung verwendet werden. Siehe auch http://dotnet-snippets.de/dns/globale-fehlerbehandlung-fuer-winform-SID983.aspx

mfG Gü


Kha - So 21.06.09 11:33

Wenn man den Main-Thread durch WaitOne anhält, wird die ganze Threading-Geschichte etwas sinnlos ;) ...



user profile iconfuncry hat folgendes geschrieben Zum zitierten Posting springen:
Wie gesagt, ich könnte Events definieren (z.B. ReadErrorOccurred, ConvertErrorOccurred) und damit die Exceptions nach außen führen - aber ist das der richtige Weg ?
Da das Thread-Management ja auch in der Gui-Klasse geschieht, fände ich hier eine lose Kupplung über Events unnötig - der ThreadPool-Aufrufer weiß doch, welche Methode er bei welchem Ergebnis aufrufen muss. Ich würde im QueueUserWorkItem also so etwas schreiben:

C#-Quelltext
1:
2:
3:
4:
5:
try {
  Invoke(HandleFooResult, NonGuiClass.Foo(...));
catch (Exception e) {
  Invoke(HandleFooException, e);
}


gfoidl - So 21.06.09 11:47

user profile iconKha hat folgendes geschrieben Zum zitierten Posting springen:
Wenn man den Main-Thread durch WaitOne anhält, wird die ganze Threading-Geschichte etwas sinnlos ;) ...


Wenn der ThreadPool verwendet wird muss irgendwann (bevor die Anwenund zu Ende ist) gewartet werden da es ein Hintergrundthread ist welcher sonst abgebrochen werden würde.
Die Taks aus Parallel-Extension funktionieren genau so - wie sonst auch. Macht also schon sinn - kommt halt darauf auf was man will.

mfG Gü


funcry - So 21.06.09 12:21

Danke für die Antworten! Aber ich stehe derzeit noch auf dem Schlauch.

Was die Sache komplizierter gestaltet, ist daß die Methode:

C#-Quelltext
1:
public void CreateSamplesAsync(CsvAggregator csvAggregator, SettingsTeach settingsTeach, string guidStr)                    


Aus der nicht-GUI Klasse SampleAggregator stammt.
Der Aufruf dieser Methode erfolgt erst aus der GUI Klasse:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
public partial class UITeachDgv : Form
    {
    [...]

private void buttonTeach_Click(object send, EventArgs ea)
        {
         [...]

            ThreadPool.QueueUserWorkItem(ignore => sampleAggregator.CreateSamplesAsync(this.csvAggregator, this.settingsTeach, this.guidStr)); // (creation of data start)
        }
}


Wie müsste ich diesen Ansatz:

C#-Quelltext
1:
2:
3:
4:
5:
try {
  Invoke(HandleFooResult, NonGuiClass.Foo(...));
catch (Exception e) {
  Invoke(HandleFooException, e);
}


umsetzen ?


Kha - So 21.06.09 15:16

user profile icongfoidl hat folgendes geschrieben Zum zitierten Posting springen:
[...]da es ein Hintergrundthread ist welcher sonst abgebrochen werden würde.
Was hier vielleicht Sinn macht - oder auch nicht. Aber einen Thread sofort nach dem Erzeugen eines anderen anzuhalten kann doch nun wirklich nie Sinn machen :?!?: .

user profile icongfoidl hat folgendes geschrieben Zum zitierten Posting springen:
Die Taks aus Parallel-Extension funktionieren genau so - wie sonst auch.
Hehe, Ich habe mich auch gefragt, wie man hier PFX einsetzen könnte. Um Invoke wird man nicht herumkommen, da ja eben der Nebenthread die Synchronisation auslöst, sobald er fertig ist.

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
Task<FooResult> fooTask;

[...]

fooTask = Task<FooResult>.Factory.StartNew(() =>
{
    try
    {
        return NonGuiClass.Foo();
    }
    finally
    {
        Invoke(FetchFooTaskResults);
    }
});

FormClosed:
    FetchFooTaskResults();

FetchFooTaskResults:
    fooTask.Wait();
    fooTask.Exception/Result...

Damit hätten wir beides drin: Der Task informiert die Form selbst, wird beim Schließen aber auch nicht abgebrochen.



user profile iconfuncry hat folgendes geschrieben Zum zitierten Posting springen:
Was die Sache komplizierter gestaltet, ist daß die Methode[...]Aus der nicht-GUI Klasse SampleAggregator stammt.
Imho sollte die Klasse nichts vom Threading wissen. Vor allem die Synchronisation kann sie ja nicht übernehmen, da Control.Invoke Winform-spezifisch ist. Wenn du es aber so lassen willst, brauchst du eben doch Events.


gfoidl - So 21.06.09 15:23

user profile iconKha hat folgendes geschrieben Zum zitierten Posting springen:
Wenn man den Main-Thread durch WaitOne anhält, wird die ganze Threading-Geschichte etwas sinnlos ;) ...

Das mit dem Beispiel hast du wohl nicht kapiert - ist aber egal. :idea:

mfG Gü


Kha - So 21.06.09 16:34

user profile icongfoidl hat folgendes geschrieben Zum zitierten Posting springen:
Das mit dem Beispiel hast du wohl nicht kapiert - ist aber egal. :idea:
Wenn sich ein Beispiel nicht auf den Kontext (Winforms) übertragen lässt, stellt sich mir eben die Frage nach dem Sinn dahinter...
Natürlich ließe sich wenigstens der Lock und das Überträger-Feld auch in Winforms umsetzen, aber da funcry ja sicher bereits Invoke zur Synchronisierung benutzt, gibt es keinen Grund dazu.

Ich würde mich aber freuen, wenn du meinen PFX-Ansatz einschätzen würdest, da du dich ja mit PFX anscheinend schon auseinandergesetzt hast. Das war nur eine fixe Idee, zu der ich gern andere Meinungen hören würde.


gfoidl - So 21.06.09 17:27

Zurück zum ursprünglichen Problem:
Dabei geht es darum dass jeder Thread seinen eigenen Stacktrace hat. Somit ist nicht ohne weiteres möglich eine Exception in einem Thread an den Aufrufer weiterzuleiten.

Mein Beispiel zeigt beispielhaft ( :D ) wie die Exception des Threads in eine Exception des Aufrufers übergeführt wird. Werden mehrere Threads verwendet so kann eine List<Exception> verwendet werden um für jeden Thread die Exception weiter zu verarbeiten.

Die Verwendung von Tasks aus dem PFX ist prinzipiell nichts anderes als die Verwendung des ThreadPool nur dass das PFX einige benutzerfreundliche Erweiterungen besitzt. Das Exception-Handling funktioniert genauso wie in meinem Beispiel beschrieben.

Somit ist es auch egal ob ich auf einen Task warte oder auf einen Thread (ist ja das "Gleiche"). Irgendwo muss synchronsiert werden, außer der "Auftrag" ist nicht wichtig und kann terminiert werden sobald das Programm (vorzeigt) beendet wird.

Wer das Beispiel nicht auf den Kontext (Winforms) übertragen kann versteht nicht was beim Multithreading vor sich geht.

Im Beispiel von funcry müsste also der innere try-catch-Block wie beispielhaft gezeigt geändert werden.Im Ereignishandler "SampleCreationCompleted" wird dann geprüft ob ein Fehler aufgetreten ist.
Das Auslösen des "SampleCreationCompleted"-Ereignis stellt eine Alternative dar die Threads sozusagen zu synchronsieren bzw. exakter über das Ende der Thread-Verarbeitung informiert zu werden.

Den gezeigten PFX-Ansatz kann ich nicht nachvollziehen. Ich sehe jedoch keinen Grund dies so umzusetzten da obige Ausführung für mich transparenter ist.

mfG Gü

PS: Meine Ausführungen des PFX beziehen sich auf die CTP (glaube June 2008).


funcry - So 21.06.09 23:14

Danke euch beiden für die ausführliche Erklärung und die gezeigten Lösungsansätze!