Autor Beitrag
OlafSt
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: Di 23.02.16 22:27 
Och nöö, nicht schon wieder... Doch, schon wieder. Denn langsam geht mir dieser .NET-"Ich will aber nicht, das du die GUI aktualisierst"-Zwang auf den sprichwörtlichen Sack.

Stellt euch vor, ihr seid Service-Techniker, der an der wertvollen Kundendatenbank eine kleine Umbauarbeit vornehmen soll. Da hat also der Entwickler ein kleines Programm gebastelt, das diesen Umbau vornimmt und mit seinen Testdaten auch durchgetestet. Funktioniert einwandfrei, ein Klick auf den "Do it"-Button und die 1500 Testdatensätze sind durchgenudelt. Nun sitzt ihr da und blöderweise ist die DB beim Kunden anstelle 1500 Datensätze satte 15Mio Datensätze groß. Beim Entwickeln klickt der Coder auf den Button "Do it" und schwupps - alles fertig. Der Servicetechniker aber klickt auf den Button und sieht: Gar nichts. Er hat nicht die blasseste Ahnung, was nun passiert. Ob überhaupt was passiert. Ob es sich lohnt, ne Kippe zu ziehen. Oder n Kaffee zu kochen. Der Kunde kommt rein und fragt: "Wie lange noch ?" worauf der Techniker sagt: Keine Ahnung, weiß es selbst nicht.

So sieht die schöne Microsoft-Welt von heute aus. Zeige bloß nicht, das irgendwas passiert, aller-aller-allerhöchstens ein paar Punkte umherfliegen lassen. Wer unter Windows 8 und höher mal ein Windows Update verfolgt hat, wird ungefähr wissen, was ich meine.

Ich mache sowas anders, damit auch der Techniker weiß, was passiert und wie lange das ungefähr noch dauern wird. Kann er doch entspannt eine rauchen gehen und n Kaffee ziehen und derweil schon mal 6 der 12 anderen LKWs mit den Blackboxes ausstatten, während der SQL-Server schwitzt - dann kann man nochmal nachsehen.

Meine übliche Vorgehensweise stammt aus Delphi-Tagen und sieht grob so aus:
ausblenden volle Höhe 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:
        private void ODS(string Msg)
        {
            if (Msg != "")
                textBox1.AppendText(Msg);
            textBox1.AppendText(Environment.NewLine);
            textBox1.Update();
        }
        private void ResolveAll_Click(object sender, EventArgs e)
        {
            //Build up ConnectionString
            int GPSCount;
            int UnresolvedCount=0;
            int NewCached = 0;
            string ConnStr = GetConnectionString();
            SqlConnection Conn = new SqlConnection(ConnStr);
            SqlCommand cmd = new SqlCommand("SELECT * FROM ResolverCache", Conn);
            SqlCommand cmd2 = new SqlCommand("SELECT * FROM GPS", Conn);
            SqlDataAdapter da = new SqlDataAdapter(cmd);
            SqlDataAdapter da2 = new SqlDataAdapter(cmd2);
            DataSet ds = new DataSet();

            ODS("Verbinde mit Datenbank...");
            Conn.Open();
            ODS("Lese Resolvercache aus...");
            da.Fill(ds, "Cache"); //Hier wirds schon spannend mit der GUI
            ODS("Lese GPS-Daten aus...");
            da2.Fill(ds, "GPS");
            //
            //und einiges mehr, das wirklich und ehrlich sehr lange dauern wird ;)
            //
        }


Sieht n Blinder mitm Krückstock, GUI blockiert, und alle meine tollen Informationen erscheinen gar nicht erst auf dem Bildschirm - tatsächlich gibts nur ein stupides "(Keine Rückmeldung)" in der Titelzeile, das den Techniker 1.) dazu neigen läßt, doch mit dem Taskmanager das Programm ausm Speicher zu werfen, bevor es die kostbare DB des Kunden zerfleddert und 2.) ihm keine Chance gibt, auch nur irgendwas zu sehen und bei Nachfragen kompetent antworten zu können.

Ich weigere mich, dem zu folgen und meine Techniker als Volltrottel dastehen zu lassen. Nun liest man ja viel über Tasks und Async/Await und das dies alles das ultimative Allheilmittel sein soll. Aber: 99,2% aller Beispiele beginnen mit:

ausblenden C#-Quelltext
1:
2:
static void Main(string[] args)
{


Da brauche ich nicht weiterlesen. Ich hab hier kein simples Kommandozeilenprogramm, das man einfach startet und dem man einfach nur beibringen will, das man das Host-Window auch während des Laufs verschieben kann. Ich habe hier ein Winforms-Programm und da brauchts mehr. Wesentlich mehr. Doch da verlassen sie uns gänzlich.

Meine Frage: Wer zeigt mir, wie man obiges Beispiel so umbaut, das der Techniker weiter beim Kunden als kompetent rüberkommt, weil er sehen kann, was passiert ? Mir ist egal, ob das mit Tasks oder Async/Await passiert - nur DoEvents() kommt nicht in Frage.

Helft mir, Freunde - ihr seid meine letzte Hoffnung.

_________________
Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4708
Erhaltene Danke: 991


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: Di 23.02.16 23:09 
Viele Wege führen nach Rom. Einer wäre ...

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
private async void ResolveAll_Click(object sender, EventArgs e)
{
    var ds = new DataSet();
    textBox1.AddLine("Lese Resolvercache aus...");
    await Task.Run(() => ds.Fill("SELECT * FROM ResolverCache""Cache"));
    textBox1.AddLine("Lese GPS-Daten aus...");
    await Task.Run(() => ds.Fill("SELECT * FROM GPS""Cache"));

    //usw.
}


ausblenden 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:
public static class ControlHelper
{
    public static void AddLine(this TextBox tb, string msg)
    {
        if (!string.IsNullOrWhiteSpace(msg))
        {
            if (tb.Text.Length > 0)
                tb.AppendText(Environment.NewLine);
            tb.AppendText(msg);
        }
    }
}

public static class DataSetHelper
{
    public static void Fill(this DataSet ds, string commandText, string tableName)
    {            
        using (var da = new SqlDataAdapter(commandText, GetConnectionString()))
        {
            da.Fill(ds, tableName);
        }
    }

    private static string GetConnectionString()
    {
        throw new NotImplementedException();
    }
}


Bedenke aber deine GUI ist jetzt bedienbar, genau das wolltest du ja auch, also mußt du verhindern das das ganze mehrmals gleichzeitig läuft.

Edit: Den SqlDataAdapter Constructor mit CommandText und ConnectionString den ich im Beispiel benutzt habe sollte man meiden. Weder das Command noch die Connection die intern gebraucht wird wird beim Dispose des SqlDataAdapter mitdisposed :motz: Also besser explizit selbstmachen.


Zuletzt bearbeitet von Ralf Jansen am Di 23.02.16 23:45, insgesamt 2-mal bearbeitet
Christian S.
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 20451
Erhaltene Danke: 2264

Win 10
C# (VS 2019)
BeitragVerfasst: Di 23.02.16 23:11 
user profile iconOlafSt hat folgendes geschrieben Zum zitierten Posting springen:
Aber: 99,2% aller Beispiele beginnen mit:

ausblenden C#-Quelltext
1:
2:
static void Main(string[] args)
{


Da brauche ich nicht weiterlesen. Ich hab hier kein simples Kommandozeilenprogramm, das man einfach startet und dem man einfach nur beibringen will, das man das Host-Window auch während des Laufs verschieben kann. Ich habe hier ein Winforms-Programm und da brauchts mehr. Wesentlich mehr.

Eigentlich irgendwie nicht. async/await und die ganzen Task-Geschichten funktionieren unabhängig davon, ob Du nun in einer Konsolenanwendung bist oder eine GUI hast. Dadurch, dass die lang laufenden Aufgaben in einen separaten Thread verfrachtet werden, kann der Hauptthread (egal welcher Anwendung) weiter Aktualisierungen vornehmen.

_________________
Zwei Worte werden Dir im Leben viele Türen öffnen - "ziehen" und "drücken".
jaenicke
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19314
Erhaltene Danke: 1747

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: Di 23.02.16 23:11 
user profile iconOlafSt hat folgendes geschrieben Zum zitierten Posting springen:
Meine übliche Vorgehensweise stammt aus Delphi-Tagen und sieht grob so aus:
Auch in Delphi macht man das normalerweise in einem Thread. Und ohne Threads oder ein Application.ProcessMessages wird auch in Delphi nichts auf der Oberfläche neu gezeichnet, da die WM_PAINT Messages usw. nicht verarbeitet werden.

user profile iconOlafSt hat folgendes geschrieben Zum zitierten Posting springen:
Da brauche ich nicht weiterlesen. Ich hab hier kein simples Kommandozeilenprogramm, das man einfach startet und dem man einfach nur beibringen will, das man das Host-Window auch während des Laufs verschieben kann. Ich habe hier ein Winforms-Programm und da brauchts mehr. Wesentlich mehr.
Was macht es für einen Sinn für ein Beispiel irgendein Fenster zu erzeugen usw.? Es wird einfach das Wesentliche gezeigt:
Wie man Tasks, Threads usw. benutzt.
Ob du das dann wie im Beispiel in einer Konsolenanwendung nutzt oder in einer Windows Forms Anwendung, macht keinen Unterschied. Du musst dann natürlich auch die Elemente der Oberfläche threadsicher ansteuern, ganz wie in Delphi.

Ein Beispiel findest du hier:
msdn.microsoft.com/d...71728(v=vs.110).aspx
Das macht im Grunde genau was du machen möchtest.
OlafSt Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: Do 25.02.16 13:11 
Hallo Freunde,

ich wußte doch, das ich hier richtig bin mit meinem Problem. Ihr habt mir da sehr weiter geholfen. Das meine GUI durch die asynchronen Aufrufe nun voll bedienbar ist, war mir schon klar - ein Problem, mit dem man schon oft konfrontiert war, besonders bei Benutzung von ProcessMessages() bzw. DoEvents(). Eine simple Bool-Variable hat hier erstmal für Ruhe gesorgt ;)

Bleiben noch ein paar Detailfragen.

Zunächst einmal habe ich meine ODS-Routine umgestrickt:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
        delegate void ODSCallback(string Msg);
        private void ODS(string Msg)
        {
            if (TB1.InvokeRequired)
            {
                ODSCallback o = new ODSCallback(ODS);
                this.Invoke(o, new object[] { Msg });
            }
            else
            {
                if (!string.IsNullOrWhiteSpace(Msg))
                    TB1.AppendText(Msg);
                TB1.AppendText(Environment.NewLine);
                TB1.Update();
            }
        }


Funktioniert prima soweit. Aber ich rufe dieses ODS u.U. mehrere 10.000 mal auf, produziere also in kurzer Zeit an die 100k "new ODSCallback"-Variablen, die der GC irgendwann mühevoll entsorgen muß. Ich nehme an, das es völlig okay ist, diesen Callback einmal zu erzeugen (im Form_Load z.B.) und dann fleißig zu benutzen ?

Das zweite Problem ist, so glaube ich, das größte Hindernis beim Verständnis der async/await-Mechanik bei mir. Es heißt, das solche Methoden grundsätzlich mit "async" deklariert werden müssen und irgendwo einmal ein "await" innerhalb dieser Methode auftauchen muß. Doch wie soll das gehen, wenn man "am Ende der Nahrungskette" angekommen ist ?
Ich bezweifle, das SqlConnection.OpenAsync() irgendwo ein await aufruft - und wenn doch, dann ist in dieser aufgerufenen Routine evtl. kein await drin - wie also muß eine Routine aussehen, die ganz am Ende der await-Arie steht und einfach kein await mehr im Code haben kann ?

Ich hoffe, ihr versteht meine Frage...

_________________
Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4708
Erhaltene Danke: 991


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: Do 25.02.16 14:43 
Ich weiß nicht genau was du mit am "Ende der Nahrungskette" meinst aber das async/await Feature ist sicher nichts was für jedes Problem geeignet ist. Es ist gut zur Threadsynchronisierung. Das ganze läßt sich genauso gut/ähnlich gut anders lösen.

Zitat:
das SqlConnection.OpenAsync() irgendwo ein await aufruft


Hier bin ich mir aber ziemlich sicher das wenn du in der Richtung irgendwas tust du höchstwahrscheinlich was falsch machst. Das öffnen ein Connection ist üblicherweise nie etwas teures. Da muß man nix asnchron machen. Eher deutet es daraufhin das du dir Probleme reinholst weil du versuchst das Connection object in verschiedenen Threads zu benutzen. Das ist gefährlich und in 99,999% aller Fälle unnötig. Und warum sollte man da drin ein await haben wollen? Du möchtest während der Sekundenbruchteile die es kostest die Connection aus dem ConnectionPool zu holen die UI ansprechbar halten?
OlafSt Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: Do 25.02.16 17:43 
user profile iconRalf Jansen hat folgendes geschrieben Zum zitierten Posting springen:
Ich weiß nicht genau was du mit am "Ende der Nahrungskette" meinst aber das async/await Feature ist sicher nichts was für jedes Problem geeignet ist. Es ist gut zur Threadsynchronisierung. Das ganze läßt sich genauso gut/ähnlich gut anders lösen.


Ich dachte mir schon, das das keiner kapiert ;) Darum das Beispiel mit OpenAsync(). Mir ist klar, das man vieles nicht mit async erschießen sollte - man stelle sich das mit einem TcpClient vor... manches verpackt man ganz wie in guten, alten Zeiten in einen guten, alten Thread. Schon klar.

Zitat:
Hier bin ich mir aber ziemlich sicher das wenn du in der Richtung irgendwas tust du höchstwahrscheinlich was falsch machst. Das öffnen ein Connection ist üblicherweise nie etwas teures. Da muß man nix asnchron machen. Eher deutet es daraufhin das du dir Probleme reinholst weil du versuchst das Connection object in verschiedenen Threads zu benutzen. Das ist gefährlich und in 99,999% aller Fälle unnötig. Und warum sollte man da drin ein await haben wollen? Du möchtest während der Sekundenbruchteile die es kostest die Connection aus dem ConnectionPool zu holen die UI ansprechbar halten?


Das ist nicht der Punkt. Ich habe OpenAsync() gewählt, weil es sehr unwahrscheinlich ist, das in OpenAsync() ein await auftaucht. Das das ne Connection aufreißt, ist hier völlig irrelevant - mal davon abgesehen, das das Eröffnen einer Connection über ein Netzwerk bei einem stark belasteten SQL-Server schon mal ein Weilchen dauern kann.

Zur Verdeutlichung meiner Frage ein simples Beispiel:
ausblenden C#-Quelltext
1:
2:
3:
4:
private async void ResolveAll_Click(object sender, EventArgs e)
{
     await DoLengthyOperation();
}


Die Methode DoLengthyOperation ist nun das Ende der Nahrungskette, denn sie selbst ruft keinerlei andere Methoden mehr auf, muß aber trotzdem nun A) mit async deklariert sein und B) irgendwo await benutzen:
ausblenden C#-Quelltext
1:
2:
3:
4:
private async void DoLengthyOperation()
{
     for(int i=0; i<1000000000u; i++); //Ewig lange Endlosschleife plus Compiler-Warnung
}


In DoLengthyOperation kann ich nirgendwo await aufrufen - es sei denn, ich mache es wie folgt:
ausblenden C#-Quelltext
1:
2:
3:
4:
private async void DoLengthyOperation()
{
     await Task.Run(() => for(int i=0; i<1000000000u; i++));
}


...und damit habe ich im wesentlichen wohl selbst meine Frage beantwortet. Denn ohne einen irgendwo im Hintergrund laufenden Task brauchts auch kein await...

Schätze, das Brett vorm Kopf ist nun weg.

_________________
Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4708
Erhaltene Danke: 991


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: Do 25.02.16 19:51 
Zitat:
Schätze, das Brett vorm Kopf ist nun weg.


Scheinbar ;)

await wirkt immmer (in erster Näherung) auf Dinge die einen Task zurückgeben auch wenn der Zusammenhang in Beispielen oft verschleiert scheint weil die Framework Methoden die für async/await vorbereitet sind ein Task Object zurückliefern obwohl man selbst von dem was die Methode tut denkt das ein reine void liefernde Methode ist. Das relevante ist die Task Klasse und async/await ein wenig Syntaxschmuck.
OlafSt Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: Fr 26.02.16 10:58 
Verstehe ich das richtig: Ich könnte auch ein Task-Objekt zurückgeben ?
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
private async void DoLengthOperation()
{
     Task t = new Task();
     //Task vorbereiten
     //Task starten
     return t;
}


Das erinnert mich dann stark an Delphis versteckten self-Parameter in Methodenaufrufen ;)

_________________
Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4708
Erhaltene Danke: 991


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: Fr 26.02.16 11:29 
Klar. Guckst du unter Implementing the Task-based Asynchronous Pattern.

Läuft aber dann in den meisten Fällen darauf hinaus das man intern Task.Run oder eine ähnliche Task Methode benutzt.
Da mag ich es lieber, wie in meinem Beispiel, ein explizites Task.Run zu sehen (mit oder ohne await).
Finde ich persönlich lesbarer ist aber natülich Geschmacksache.

Moderiert von user profile iconTh69: URL-Titel geändert.
OlafSt Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: Fr 26.02.16 16:02 
Okay, dann hab ich es jetzt endgültig gerafft.

Schon komisch, das es keinem auffällt, das die Methode DoLengthyOperation() als void deklariert ist, aber ein Task-Objekt per return zurückgibt...

However. Ich präferiere auch ein explizites Task.Run(), da weiß man gleich, woran man ist. Gedankenstütze im Alter und so ;)

_________________
Lies, was da steht. Denk dann drüber nach. Dann erst fragen.