Entwickler-Ecke

Sonstiges (.NET) - brauche hilfe beim nutzen von Multithreading bzw. TPL


avoid - Fr 27.07.12 23:25
Titel: brauche hilfe beim nutzen von Multithreading bzw. TPL
ich hab schon das beispiel mit backgroundworker und der progressbar hinter mir.
auch einzelne Threads hab ich schon testhalber async laufen lassen.
aber im großen und ganzen hab ich die funktionsweise noch nicht verstanden.

mein problem:
ich lese daten mittels webclient ein (blockt mir die anwendung ca. 12 sec)
und ich verarbeite die daten relatiev aufwendig (blockt auch ein bis zwei sekunden),
dann zeige ich die daten noch in einem datagridview an und speicher sie dann in eine xml (das geht recht fix).
am datagridview hängen noch ein bindignsource und ein dataset dran.
das alles klappt bis auf das gelegentliche hängen, bis jetzt ohne Multithreading.

die hänger sind aber nicht gerade die feine art und dafür kann man arbeit ja auslagern in eigene threads.
ob altes Multithreading oder aktuelle TPL ist mir persönlich wurscht,
soviel rechenarbeit das es einen unterschied macht ist es nicht.

da ich warscheinlich meine anwendung so wie so nochmal neu anfangen muß zwecks übersichtlichkeit,
würde ich mich freuen wenn ihr mir dabei helfen könntet diese ablauftechnisch passend zu gestalten,
um zukünftig keine hänger mehr zu haben.

ich meine damit, hinweise was ich dabei beachten sollte, wie ich variablen richtig übergebe,
und was für oben beschriebene anwendung noch wichtig ist.

----edit----

so ich hab mir jetzt also eine neue winforms anwendung
mit folgenden steuerelementen zusammen geklickt.

dataSet1
- dataTable1
-- dataColumn1 = Datum
-- dataColumn2 = Startzeit
-- dataColumn3 = Dauer
-- dataColumn4 = System

bindingSource1
- DataSource = dataSet1

dataGridView1
- DataSource = table1BindingSource

backgroundWorker1

zwei beispiel dateien die ich mit webclient abrufe habe ich mal angehangen.
ich werde sie bei gelegenheit auf nen webspace hoch laden um es richtig zu testen.

wie gehts jetzt weiter? wie bau ich das abrufen der daten, das verarbeiten und das darstellen so in ein oder zwei backgroundworker ein das es die anwendung bei mehr daten nicht hängen lässt?

---nachtrag---
danke für das verschieben, auch wenn ich weiterhin der meinung bin dass,
das zusammenspiel von steuerelementen in winforms besser aufgehoben ist.

gruß, avoid.


Niko S. - Sa 28.07.12 22:41

Ich arbeite mich gerade selbst in C# ein und werde demnächst auch auf eine Thread / Task umsetzung stoßen von daher empfehle ich dir einfach mal folgendes zu lesen:
http://openbook.galileocomputing.de/visual_csharp_2010/visual_csharp_2010_11_001.htm#mjd93ae35731528c9cb9d0d6cfbd011600

Ich finde das ganz gut erklärt und recht übersichtlich. Kleinere Tests damit haben auch super geklappt.

Prinzipell hast du ja 3 Schritte die Hintereinander ausgeführt werden müssen. Einlesen; Verarbeiten; Ausgeben;
Da brauchst du nur einen extra Thread. Der Hauptthread ruft das ding eben auf und wartet bis es fertig ist und währenddessen gibt 's halt nen Fortschrittsbalken oder sowas.
Das ist eigentlich schon alles.


avoid - Sa 28.07.12 23:00

hi Niko,
genau diese bettlektüre und deren kleinen bruder hab ich hier rum liegen.
doch weil ich daraus nicht ausreichend schlau werde dieser beitrag.

meine hauptprobleme sind, das der backgroundworker meine variablen nicht verwenden will und erst recht keine ergebnisse seiner arbeit zurück gibt.
das was eben jedem anfänger kopfzerbrechen veruhrsacht der damit mehr machen will als bis 100 zu zählen. ;)
hab eig. auf ein par code fragmente und nähkästchentips gehofft um typische anfängerfehler garnicht erst zu machen.


Niko S. - So 29.07.12 09:23

Ah jetzt sehe ich. So weit war ich dann leide doch auch noch nicht.
Naja das Problem liegt ja dabei, dass man Threads nur als Static deklarieren kann. Und diese können auch jeweils nur auf Static variablen zugreifen.
Wäre ja auch nicht so das Thema. Benutzt manhalt nen Static variable und sorgt dafür, dass 2 Threads nicht gleichzeitig darauf rum ackern.

Ansonsten gäbe es noch die Möglichkeit über die TPL die Task klasse zu benutzen. Die hat nen generic Task<TResult> oder so.
Aber selbst kann ich dir gerade keinen Codeschnipsel geben sorry. Aber da mich das selbst auch interessiert, schau ich mir das auch an.
Da gibt's doch bestimmt noch bessere tricks als statics.


Christian S. - So 29.07.12 10:17

Hallo!

Also irgendwie ist das hier zu unspezifisch. Man kann ja jetzt nicht fünfzig Seiten mit Tipps schreiben in der Hoffnung, dass der Richtige dabei ist ;-) Da erscheint mir effizienter zu sein, wenn dann bei konkreten Probleme geholfen wird :-)

Generell erscheint mir der BackgroundWorker inzwischen ein bisschen veraltet zu sein und die TPL das Mittel der Wahl, weil man mit der auch mehrere Prozessorkerne recht einfach ausnutzen kann (Stichwort "parallelisierte for-Schleifen"). Außerdem bleibt der Code dem nicht-parallelisierten sehr ähnlich, was IMHO die Lesbarkeit stark erhöht.

Bei etwas konkreteren Fragen kann man dann sicherlich auch konkreter helfen :)

Viele Grüße,
Christian


Niko S. - So 29.07.12 10:29

Naja doch das ist schon Konkret:
Man braucht eine vernünftige Vorgehensweise Threads / Tasks zu vermitteln, dass Sie etwas bekommen und wieder zurück geben.

Das Hauptprogramm soll ja etwas ausführen was das Programm blockiert. Also braucht man nen extra Thread / Task.
Dieser Thread/Task soll allerdings Parameter bekommen. Gut nicht wirklich schwer. Und er soll etwas zurückliefern. (Nur mit Tasks Sinnvoll möglich oder?).
Dann soll das Hauptprogramm den Thread/Task ausführen und darauf warten dass er fertig ist, dabei aber noch bedienbar bleiben.

Jetzt ist die konkrete Frage: Wie realisiert man das am besten in C#.


jfheins - So 29.07.12 10:45

Ich kann dir hier mal ein kurzes Beispiel geben, denn ich habe kürzlich sowas gemacht ^^
( http://www.entwickler-ecke.de/viewtopic.php?t=109745&postorder=asc&start=40 )

Folgendes solltest du dir voher überlegen:
- Brauchst du einen Rückgabewert?
- Welche Parameter brauchst du?
- Möchtest du die Aufgabe abbrechen können?

Ich brauchte zwei Parameter, wollte die Aufgabe abbrechen können und wollte das Ergebnis anzeigen. Und es gab nur einen einzigen Task, der im Hintergrund gearbeitet hat.
Der Code zum Starten ist folgender:

C#-Quelltext
1:
2:
3:
            CancelToken = new CancellationTokenSource();
            Task = Task.Factory.StartNew(() => Calculate(startnum, (int)IterNud.Value), CancelToken.Token);
            Task.ContinueWith((x) => DisplayResult());

Der restliche Code:

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:
    public partial class Form1 : Form
    {
        CancellationTokenSource CancelToken;
        Task Task;
        CalcResult SavedState;

        private void DisplayResult()
        {
            ResultTxt.Invoke((Action)delegate
            {
// Ergebnisse anzeigen; GUI Interaktion muss im Hauptthread stattfinden, deshalb das Invoke.
                ResultTxt.Text += SavedState.ToString() + "\r\n\r\n";
            });
        }

        private void Calculate(Number start, int iterations)
        {
            // Hier ganz viel Rechnen
// Ich darf hier asynchron in SavedState reinschreiben,
// weil sichergestellt ist, dass nirgendwo sonst parallel gelesen oder geschrieben wird
            SavedState.Number = 1234;
            SavedState.Iterations += 32101;
        }

Der CancelToken wird gebraucht, um die Aufgabe abbrechen zu können. Die Funktion in der die Rechnung steckt ist Calculate() und wird hier als Lambda-Ausdruck mit 3 Parametern übergeben. Das Anzeigen der Ergebnisse wird auf jeden Fall nach der Berechnung erfolgen und automatisch angestoßen: Sobald der Task mit Calculate() fertig ist, wird DisplayResult() aufgerufen.

Ich hoffe das ist dir jetzt etwas klarer geworden :)


Niko S. - So 29.07.12 12:03

Ja vielen dank das klappt soweit ganz gut.
Das ganze ist doch deutlich anders als Delphi bzw Lazarus.
Mal sehen ob ich da Multithreaded Sockets hinbekomme.

Eine Frage noch: Invoke brauch ich nicht für Konsolenanwendungen oder?


avoid - So 29.07.12 12:05

hi christian,

über parallel.for() bin ich auch schon gestolpert und dachte das ist das richtige für mich,
doch da hab ich noch immer das selbe problem das nico richtig erkannt hat.

ich sehe mir gerade jfheins code und beitrag an, lese dann nochmal etwas im dicken Gallileo schinken.
und werd dann mal ne runde rum probieren und kucken ob ich es hin bekomme.
dann gibts auch nen code von mir, den ihr nach belieben zerpflücken könnt.

p.s. der backgroundworker ist eig. mein mittel der wahl (weil vorhanden),
ich hab mich an das komponenten zusammen klicken gewöhnt und will möglichst wehnig selbst konstruieren,
das reduziert mir die gefahr über die zeit den überblick zu verlieren.
(wisst schon, wenn man nach monaten wieder in nen code kuckt und sich fragt: "was zum teufel hab ich da gemacht".)


Niko S. - So 29.07.12 12:16

Probier doch mal das von jfheins zusammen mit Task<TResult> (letztes beispiel bei dem Galileo TPL teil).
Das müsste eigentlich bereits erfüllen was du brauchst.

Zudem wenn du den Überblick verlierst gibts auch nen Tipp. Reduzier die Aufgaben in Sinnvolle Klassen und mach ein paar kleinere Beschreibungen und Kommentare.
Auch wenn es lästig ist, aber mir hat's dann geholfen auch nach Monaten innerhalb 15 Minuten den Überblick wieder zu erlangen.


avoid - So 29.07.12 21:10

ich hab jetzt erst mal das dataset und das bindingsource raus geworfen.
dann hab ich folgendes ausprobiert.


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:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    for (int i = 1; i <= 5; ++i)
    {
        bnummer = i;
        WebClient client = new WebClient();
        client.Headers.Add("user-agent""Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.0.3705;)");
        string url = "http://127.0.0.1/beispiel" + bnummer + ".txt";
        try
        {
            daten += client.DownloadString(url);
        }
        catch
        {
            //
        }
    }

    //dataGridView1.Rows.Clear();
    string[] datenzeilen = daten.Split(new Char[] { '\n''\r' }, StringSplitOptions.RemoveEmptyEntries);
    string[] row;
    for (int i = 0; i < datenzeilen.Count(); i++)
    {
        string zeile = datenzeilen[i].ToString();
        string[] zeilen_array = zeile.Split(new Char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
        string datum = zeilen_array[0];
        string uhrzeit = zeilen_array[1];
        string inhalt = "";
        for (int y = 2; y < zeilen_array.Length; y++)
        {
            inhalt = inhalt + " " + zeilen_array[y];
        }
        row = new string[] { datum, uhrzeit, inhalt };
        //dataGridView1.Rows.Add(row);
    }
}

nun hab ich das problem das die beiden auskommentierten bereiche schon mal nicht funktionieren.
wenn ich die untere hälfte vom code in "RunWorkerCompleted" packe gehts aber
dann hängt die anwendung wieder solang die for schleife für die ausgabe ackert.


jfheins - So 29.07.12 21:15

user profile iconavoid hat folgendes geschrieben Zum zitierten Posting springen:
nun hab ich das problem das die beiden auskommentierten bereiche schon mal nicht funktionieren.

Logisch. GUI Aktionen müssen im Hauptthread stattfinden. Das ist in Delphi genau so wie in C#.

user profile iconavoid hat folgendes geschrieben Zum zitierten Posting springen:
wenn ich die untere hälfte vom code in "RunWorkerCompleted" packe gehts aber
dann hängt die anwendung wieder solang die for schleife für die ausgabe ackert.

Jaaa .... aber das sollte ja nicht allzu lang sein !?
Wieviel haust du denn da rein?


avoid - Mo 30.07.12 10:33

aktuell sind es 5 dateien mit je 900 zeilen.
das ist so das maximum was später an daten aufkommen wird.

kann ich die ausgabe irgendwie so umbauen, das die ausgabe schon vorbereitet wird
und in "RunWorkerCompleted" nur noch darzustellen ist?

ich meine wenn ich daten nicht an das "dataGridView1" übergeben kann,
kann ich dann zumindet die daten in ein "dataset" packen und dieses am ende einfach ins view laden?


jfheins - Mo 30.07.12 14:33

Hmmm ich habe jetzt noch nie was mit dem Backgroundworker gemacht.
Aber 5000 Zeilen zu einem GUI Element hinzufügen darf nicht so lange dauern.
(Ist natürlich auch nicht soooo irre sinnvoll, aber es sollte schon in ner halben Sekunde erledigt sein.)
Kannst du mal das ganze Projekt anhängen? Dann kann ich das mal angucken und vielleicht finde ich da ja was...


avoid - Mo 30.07.12 15:06

leider kann ich es gerade nicht anhängen, evtl. heut abend.
aber ich kann dir sagen worann es liegt.

wenn ich die daten mit data += client.DownloadString(url); einlese,
wie ich es aktuell mache, dann gibt das einen langen string.

in der ausgabe zerpflück ich diesen string mit split() in die einzelnen zeilen.
danach durchlaufe ich jedezeile und zerlege sie in datum,uhrzeit und was sonst noch so in der zeile enhalten ist,
bevor ich die zeile an das datagridview aus gebe.
danach folgt die nächste zeile ....

weil das so weder mit dem backgroundworker noch anderen Multithreading möglichkeiten geht wegen GUI-Aktion,
bin ich jetzt dabei die daten erst mal in die tabelle eines dataset zu packen.
nach dem durchlaufen des worker zeige ich dann den dataset inhalt im datagridview an.
wenn's so klappt.

wenn nicht hab ich keine lust mehr. :(


jfheins - Mo 30.07.12 15:38

Also laut dem hier:
http://social.msdn.microsoft.com/Forums/en-US/winformsdatacontrols/thread/80505719-3af8-45a9-acf9-b7183ce63234/
Solltest du darüber nachdenken, das Control an eine eigene Liste zu binden :wink:

Als zweitbeste Lösung kannst du eine List<DataGridViewRow> benutzen, befüllen und über dataGridView1.Rows.AddRange(rows.ToArray) hinzufügen.


avoid - Mo 30.07.12 16:26

ich hab jetzt zwar was laufen aber mehr schlecht als recht.


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:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    tag = DateTime.Now.ToString("yyyyMMdd"); // tag der im LiveView zu sehen ist.
    uhrzeit = DateTime.Now.ToString("HH:mm:ss"); // aktuelle uhrzeit
    WebClient client = new WebClient();
    client.Credentials = new NetworkCredential("user""pass");
    client.Headers.Add("user-agent""Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.0.3705;)");
    string url = "http://" + ip + "/" + tag + ".txt";
    try
    {
        backgroundWorker1.ReportProgress(1); // meldet status
        data += client.DownloadString(url); // abrufen
        backgroundWorker1.ReportProgress(100); // meldet status
    }
    catch (WebException we)
    {
        //MessageBox.Show(we.Message + "\n" + we.Status.ToString();
        backgroundWorker1.ReportProgress(98); // meldet status
    }

    string[] datenzeilen = data.Split(new Char[] { '\n''\r' }, StringSplitOptions.RemoveEmptyEntries);
    dataTable1.Clear();
    for (int i = 0; i < datenzeilen.Count(); i++)
    {
        string zeile = datenzeilen[i].ToString();
        string[] zeilen_array = zeile.Split(new Char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
        int lenge = zeilen_array.Count();
        string info1, info2, status = "", startzeit, dauer, info3, info4, info5 = "";
        startzeit = zeilen_array[0].Replace('.'':');
        info2 = zeilen_array[1];
        info1 = zeilen_array[2];

        // noch optimieren
        status = zeilen_array[3];

        TimeSpan timeSpans = TimeSpan.Parse(startzeit);
        TimeSpan timeSpane = TimeSpan.Parse(uhrzeit);
        TimeSpan comp = timeSpane - timeSpans;
        dauer = Convert.ToString(comp);
        string[] lza = zeilen_array[lenge - 1].Split(new Char[] { '-' }, StringSplitOptions.RemoveEmptyEntries);
        try
        {
            info3 = lza[0];
            info4 = lza[1];
            try
            {
                info5 = lza[2];
            }
            catch
            {
                info5 = "";
            }
        }
        catch
        {
            info3 = "";
            info4 = "";
        }
        DataRow myRow;
        myRow = dataTable1.NewRow();
        myRow["typ"] = "";
        myRow["info1"] = info1;
        myRow["info2"] = info2;
        myRow["info5 "] = info5 ;
        myRow["status"] = status;
        myRow["startzeit"] = startzeit;
        myRow["dauer"] = dauer;
        myRow["info3"] = info3;
        myRow["info4"] = info4;
        dataTable1.Rows.Add(myRow);
    }
}



C#-Quelltext
1:
2:
3:
4:
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    dataGridView1.Refresh();
}

der worker wird alle 5sec. von einem timer aufgerufen, wenn er nicht gerade arbeitet.

problem dabei, das datagridview ist hoffnungslos damit überfordert und sobald ich die größe des form ändere,
schmiert mir die anwendung ab weil sie den index für die scrollbalken nicht richtig bestimmen kann.

jetzt dauert quasi der refresh zu lang.
aber naja, zumindest hängt nix.

eig. müßte ich jetzt noch vor dem darstellen den inhalt jeder zeile auf start und stop infos abfragen.
um am ende nur die übrig gebliebenen daten an zu zeigen.
aber ich mag jetzt nicht mehr. :(

sorry leute aber so frustrierend wie der dreck mit dem datagridview & brackgroundworker war noch nichts in c#.
weil auch das thema mittlerweile nicht mehr dem titel des beitrag entspricht, mach ich hier jetzt nen cut.
trotzdem danke für eure hilfe.

gruß, avoid


jfheins - Mo 30.07.12 18:23

Du hast meinen Post direkt über deinem nicht gelesen, oder?

Weil du immer noch folgendes Konstrukt hast:


C#-Quelltext
1:
2:
3:
4:
5:
6:
    for (int i = 0; i < datenzeilen.Count(); i++)
    {
        //...
        dataTable1.Rows.Add(myRow);
    }
}


Und es ist langsam, jede Zeile einzeln hinzuzufügen!
==> Erstelle eine lokale Variable rows vom Typ List<DataGridViewRow> und füge dieser dann alle Zeilen hinzu. Erst nach der for-Schleife fügst du dann alle Zeilen auf einmal ein, mittels

C#-Quelltext
1:
 dataGridView1.Rows.AddRange(rows.ToArray)                    


Bitte einmal ausprobieren :wink:


avoid - Mo 30.07.12 21:01

ich hab deinen beitrag oben nur kurz überflogen.
was würde sich damit groß ändern?

ob ich im hintergrund die daten zeile für zeile in die tabelle vom dataset packe und dann nur das datagridview refreshe oder ob ich im hintergrund zeile für zeile in eine list packe und diese dann auf einmal ins datagridview lade, kommt doch auf's selbe raus oder nicht?

problem ist die daten in die anzeige zu bringen ohne das die anwendung stockt.
ich werd das mit der list morgen noch ausprobieren.
aber dran glauben, das es was ändert, tu ich nicht.

gruß, avoid


jfheins - Mi 01.08.12 21:53

user profile iconavoid hat folgendes geschrieben Zum zitierten Posting springen:
was würde sich damit groß ändern?
[...], kommt doch auf's selbe raus oder nicht?
[...]aber dran glauben, das es was ändert, tu ich nicht.

Da du noch nicht geantwortet hast, nehme ich an du hast es noch nicht probiert. Zur Erinnerung: In dem verlinkten Thread hat diese Technik die Laufzeit von 12s auf 200ms gedrückt. Nur so als Tipp ;)


avoid - Do 02.08.12 12:49

ich hab es ausprobiert, doch irgendwo hat sich jetzt ein fehler eingeschlichen.
darum versuche ich gerade den kompletten ausgabe bereich neu zu schreiben.

der fehler wird in diesem bereich ausgelöst:

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
private void dataGridView1_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
    if (e.RowIndex >= 0)
    {
        string ind = dataGridView1[4, e.RowIndex].Value.ToString();
        if (ind == "blablabla")
        {
            e.CellStyle.BackColor = Color.DarkGray;
        }
    }
}


damit färbe ich die zeilen im dataGridView1 passend zum text, in der spalte mit dem index 4.
der fehler sagt so viel aus wie: "das bindingsource kann nicht auf sich selbst zu greifen".
da der fehler, trotz der selben daten, sporadisch auftaucht ist es etwas komisch.
genaue meldung im anhang.

alles andere läuft jetzt wie gewünscht.
ich poste bei gelegenheit, wie ich es gelöst habe.

-----nachtrag-----

ok, is aber auch klar wenn ich nur den e.RowIndex prüfe. ;)

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
if (e.ColumnIndex >= 0 && e.RowIndex >= 0 && e.RowIndex < dataTable1.Rows.Count)
{
    string ind = dataTable1.Rows[e.RowIndex].ItemArray[4].ToString(); 
    if (ind == " bla bla bla")
    {
        e.CellStyle.BackColor = Color.Green;
    }
    if (ind == " müll")
    {
        e.CellStyle.BackColor = Color.Blue;
    }
}

das ist wohl besser dafür geeignet.

des weiteren hab ich blöderweise das dataGridView1 abgefragt noch bevor da daten drin waren.
jetzt frage ich dataTable1 ab wo die daten enthalten sind bevor sie im dataGridView1 angezeigt werden.

Moderiert von user profile iconChristian S.: Beiträge zusammengefasst

Frage beantwortet.

--nachtrag--
sorry hab antworten geklickt anstelle Edit. naja bei der Uhrzeit ... ;)