Autor |
Beitrag |
OlafSt
      
Beiträge: 486
Erhaltene Danke: 99
Win7, Win81, Win10
Tokyo, VS2017
|
Verfasst: 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:
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) { 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"); ODS("Lese GPS-Daten aus..."); da2.Fill(ds, "GPS"); } |
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:
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
      
Beiträge: 4708
Erhaltene Danke: 991
VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
|
Verfasst: Di 23.02.16 23:09
Viele Wege führen nach Rom. Einer wäre ...
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"));
} |
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  Also besser explizit selbstmachen.
Zuletzt bearbeitet von Ralf Jansen am Di 23.02.16 23:45, insgesamt 2-mal bearbeitet
|
|
Christian S.
      
Beiträge: 20451
Erhaltene Danke: 2264
Win 10
C# (VS 2019)
|
Verfasst: Di 23.02.16 23:11
_________________ Zwei Worte werden Dir im Leben viele Türen öffnen - "ziehen" und "drücken".
|
|
jaenicke
      
Beiträge: 19314
Erhaltene Danke: 1747
W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
|
Verfasst: Di 23.02.16 23:11
OlafSt hat folgendes geschrieben : | 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.
OlafSt hat folgendes geschrieben : | 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 
      
Beiträge: 486
Erhaltene Danke: 99
Win7, Win81, Win10
Tokyo, VS2017
|
Verfasst: 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:
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
      
Beiträge: 4708
Erhaltene Danke: 991
VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
|
Verfasst: 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 
      
Beiträge: 486
Erhaltene Danke: 99
Win7, Win81, Win10
Tokyo, VS2017
|
Verfasst: Do 25.02.16 17:43
Ralf Jansen hat folgendes geschrieben : | 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:
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:
C#-Quelltext 1: 2: 3: 4:
| private async void DoLengthyOperation() { for(int i=0; i<1000000000u; i++); } |
In DoLengthyOperation kann ich nirgendwo await aufrufen - es sei denn, ich mache es wie folgt:
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
      
Beiträge: 4708
Erhaltene Danke: 991
VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
|
Verfasst: 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 
      
Beiträge: 486
Erhaltene Danke: 99
Win7, Win81, Win10
Tokyo, VS2017
|
Verfasst: Fr 26.02.16 10:58
Verstehe ich das richtig: Ich könnte auch ein Task-Objekt zurückgeben ?
C#-Quelltext 1: 2: 3: 4: 5: 6: 7:
| private async void DoLengthOperation() { Task t = new Task(); 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
      
Beiträge: 4708
Erhaltene Danke: 991
VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
|
Verfasst: 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 Th69: URL-Titel geändert.
|
|
OlafSt 
      
Beiträge: 486
Erhaltene Danke: 99
Win7, Win81, Win10
Tokyo, VS2017
|
Verfasst: 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.
|
|
|