Laden...

C# Task arbeitet sequenziell trotz Async/Await

Erstellt von CSharpFreak vor 10 Jahren Letzter Beitrag vor 10 Jahren 7.801 Views
C
CSharpFreak Themenstarter:in
42 Beiträge seit 2014
vor 10 Jahren
C# Task arbeitet sequenziell trotz Async/Await

Hallo -

ich habe ein Problem mit meiner Anwendung ich starte um die ~40 Tasks, welche in eine DB schreiben.

Die Tasks werden scheinbar nicht oder nur "sequenziell" ausgeführt.

Starte ich statt den Tasks stattdessen Threads werden viel mehr Datensätze in kürzerer Zeit in die DB geschrieben.

Nun wollte ich mal fragen wieso das so ist bzw. ob ich das ändern könnte?

Codebeispiel wie ich einen Task starte:

Task TestTask = new Task(async () =>
            {
			// Schreibe in eine DB
			}).Start();

MfG

16.828 Beiträge seit 2008
vor 10 Jahren

Arg viel schlimmer kannst Du hier async nicht missbrauchen. Schau Dir an, wie man async mit Tasks richtig verwendet - so jedenfalls nicht.
Das async kannst Du hier völlig weglassen - bring hier 0.

Korrekt wäre zudem auch das Starten über Task.Factory.StartNew bzw. Task.Run

Bedenke auch, dass bis auf wenige Ausnahmen Datenbankverbindungen nicht Thread-safe sind.
Für Mass-Inserts verwendet man i.d.R Bulk.

C
CSharpFreak Themenstarter:in
42 Beiträge seit 2014
vor 10 Jahren

Danke für die Antwort benutze nun auch Task.Run.

Aber im Bezug auf das async weiß ich nicht wie dies anders geregelt werden sollte... ohne dem kann ich in der Funktion keine Asynchronen aufrufe machen.

Oder sollte man dafür wieder eine Task erzeugen?

Parallel Programming with .NET: Task.Run vs Task.Factory.StartNew

Danke für den hinweis mit Bulk - hört sich vielversprechend an

C
2.121 Beiträge seit 2010
vor 10 Jahren

Ich hab auch schon meine Erfahrungen mit Tasks. Tasks sind dann gut wenn man die CPU wirklich gut auslastet. Das tun deine anscheinend nicht.
Auch wenns ketzerisch klingen mag, nutz einfach Threads für sowas. Die werden nicht im Hintergrund von einer Logik gesteuert die auf deinen Anwendungsfall wahrscheinlich ganz einfach nicht zutrifft.

16.828 Beiträge seit 2008
vor 10 Jahren

Davon abgesehen, dass im Hintergrund von Task ein Thread aus dem ThreadPool werkelt (Task ~= Thread, nur mit Scheduler-Logik) läuft ein Task bereits asynchron. Da noch ein async reinzapfen ist so unnötig wie nen Kropf.

Ich vermute also ein insgesamtes Code-Logik Problem und kein Problem von Task/Threads und zeigt eher, dass man das async/await-Pattern nicht verstanden hat.
Hinter async/await steckt nämlich ebenfalls ein Task, den der Compiler erstellt.

C
CSharpFreak Themenstarter:in
42 Beiträge seit 2014
vor 10 Jahren

Möglich das ich es nicht richtig verstehe, da ich erst neulich mit Tasks angefangen habe bzw. mich momentan immer noch darin einlese.

Evtl. mal ein größeres Beispiel meinerseits:

public class DBInsertClient
{
	public Task Routine;
	public CancellationTokenSource AsyncCancelTokenSource = null;
	
	public Boolean StartInsertDataBaseRoutine()
	{
		Routine = Task.Run(async () =>
				{
					using (MySqlCommand mySQL_CMD = new MySqlCommand("INSERT ......", Connection))
					{
						mySQL_CMD.Parameters.Add("?Value", MySqlDbType.UInt32).Value = 0;
						await mySQL_CMD.ExecuteNonQueryAsync(AsyncCancelTokenSource.Token);
					}
				}
				,(AsyncCancelTokenSource = new CancellationTokenSource()).Token);
		return Routine.Status == TaskStatus.RanToCompletion;
	}
}

Von dieser konstellation starte ich mehrmals die StartInsertDataBaseRoutine aber die die CPU steigt nicht in die höhe bzw. die Einträge dauern ewig (muss ich noch auf Bulk umschreiben) aber dies soll den Aufbau zeigen wie ich die Tasks erzeuge. Es wäre daher sehr nett, wenn mir wer das Beispiel so umgestellt das ich die Pattern nicht missbrauche?

16.828 Beiträge seit 2008
vor 10 Jahren
  1. kann so nich _sicher _umgeschrieben werden, da die Datenbankverbindung nicht multi-thread-fähig ist.
  2. wäre das absolut nicht im Sinne einer Abstraktion der Datenschicht zB via Repository Pattern
  3. was soll Dir das bringen? async/wait ist für etwas ganz anderes gedacht als Dein Vorhaben: Asynchrone Programmierung mit Async und Await
    Der Sinn ist jedenfalls kein Mass-Insert mit Fire and Forget.

Die korrekte Schreibweise hier wäre aber


public async Task<Int32> NameDerMethode(...... connection)
{
  using (MySqlCommand mySQL_CMD = new MySqlCommand("INSERT ......", connection))
  {
    mySQL_CMD.Parameters.Add("?Value", MySqlDbType.UInt32).Value = 0;
    return mySQL_CMD.ExecuteNonQueryAsync(AsyncCancelTokenSource.Token);
  }
}

async/await ist jedenfalls nichts für Anfänger. Man muss das wirklich verstehen, welche "Magie" Microsoft hier im Compiler umgesetzt hat und wofür das ist.

Fazit: dieses Konzept ist das falsche für eine Vielzahl von DB Submits.
Korrekt wären Bulk Inserts, die in MySQL "Load Data" (LOAD DATA INFILE) heißen (.NET -> MySqlBulkLoader ).

Alternativ kann man mehrere Values zeitgleich submitten (Value1, Value2, Value...).
Und dann gehts eben an die Optimierung der Geschwindigkeit. Table sperren, Indexierung temporär deaktivieren, etc etc...

211 Beiträge seit 2008
vor 10 Jahren

Du verwendest async/await hier einfach "etwas" kompliziert und ich glaube du hast noch ein Verständnisproblem?

Besser wäre;



public class DBInsertClient
{
    public Task Routine;
    public CancellationTokenSource AsyncCancelTokenSource = null;

    public async void StartInsertDataBaseRoutine()
    {

                    using (MySqlCommand mySQL_CMD = new MySqlCommand("INSERT ......", Connection))
                    {
                        mySQL_CMD.Parameters.Add("?Value", MySqlDbType.UInt32).Value = 0;
                        await mySQL_CMD.ExecuteNonQueryAsync(AsyncCancelTokenSource.Token);
                    }
                }
    }
}

ADO.NET bietet dir hierbei bereits eine NonQueryAsync an, diese liefert einen Task den du "awaiten" kannst.

Kontakt & Blog: www.giesswein-apps.at

F
10.010 Beiträge seit 2004
vor 10 Jahren

Und massen inserts gehören immer in eine Transaction, ist hier aber bestimmt hundert mal beschrieben.

W
955 Beiträge seit 2010
vor 10 Jahren

... und außerdem kann man das Statement einmal "Preparen", dann in der Schleife nur noch die Parameter befüllen und ausführen lassen. Dann muß das Statement vom DB-Server nur einmal geparst werden und ist dann auch schnell genug.

C
CSharpFreak Themenstarter:in
42 Beiträge seit 2014
vor 10 Jahren

Ja - ich habe das Prinzip der Tasks wohl nicht richtig verstanden..

Ich dachte die CPU wird bis zum Limit beschäftigt dem ist aber nicht so er startet soviel Tasks wie er der CPU "zumutet" unabhängig davon wie anspruchsvoll die Task ist 😕. Ich dachte jedoch das wenn eine Task im await geht das die nächste dran kommt.

Für mein vorhaben muss ich wohl einen eigenen kleinen Scheduler coden.

Preparen meinst du die DB oder die Methode prepare des MySqlCommand?

16.828 Beiträge seit 2008
vor 10 Jahren

Nein. Er startet auch nicht so viele Tasks wie er will.
DU erstellst die Tasks. Der Unterschied ist dass der Scheduler sich um die Abarbeitung kümmert.

Sprich nur weil Du 1000 Tasks erstellt heisst das nicht, dass 1000 Tasks parallel sind. Sondern eben 8, dann mal 10, dann mal 5. Je nach Ressourcen.
Hingegen startest Du 1000 Threads heisst das, dass auch 1000 Thread aktiv sind.

Wenn Du Dich nicht sauber damit beschäftigst was ein Thread ist, was ein Task ist und was await ist, dann wirst Du immer und immer wieder diese Fehler machen, da Du einfach nicht verstehst, was passiert.
Rumprobieren bei Tasks/Threads ist das aller, aller, aller schlechteste, was man machen kann.

Und nochmal, ich weiß nicht ob Dus nicht verstanden hast oder verstehen willst: Für Dein Vorhaben sind paralelle Tasks oder Threads ein absolutes NoGo!
Es ist NICHT Multi-Threading fähig!

Verwende entweder Bulk Insert oder Transactions und verzichte auf den Task/Thread-Quatsch an dieser Stelle.

C
CSharpFreak Themenstarter:in
42 Beiträge seit 2014
vor 10 Jahren

Dann verstehe ich nicht wie ich mein Problem sonst lösen soll wenn ich keine Threads/Tasks verwenden soll weil's ein NoGo ist.

Die DB-Einträge sind ja nur ein Teil davon was in der Task/Thread ablaufen soll.
Diese läuft normalerweise in einer Endlosschleife und pullt Daten von einem Webserver und trägt diese wiederum in die DB ein. Dabei möchte ich nicht nur eine Webseite abfragen sondern mehrere... was sollte ich denn sonst tun? Mehrere Anwendungen starten??

Das mit der DB habe ich schon verstanden das diese nicht Multi-Threading fähig ist und ich Bulk nutzen sollte bzw. auch machen werde, das ändert jedoch nichts daran das ich meiner Meinung nach Tasks/Threads benötige um das Problem zu lösen bzw. Parallel Webseiten abzufragen?

W
955 Beiträge seit 2010
vor 10 Jahren

Hallo,

Dabei möchte ich nicht nur eine Webseite abfragen sondern mehrere.. Wenn die Seiten getrennt voneinander sind und nichts miteinander zu tun haben könntest Du tatsächlich komplett getrennt behandeln mit mehreren Tasks, wobei jede ihre eigene DB-Verbindung bekommt.

Zur DB: mach mal folgendes:

  1. Erschaffe das SQL-Statement als MysqlCommand. Die einzufügenden Spaltenwerte schreibst Du als DB-Parameter ohne Werte.
  2. Erstelle die Parameter zum Command (Name + Typ).
  3. Transaktion beginnen
  4. Das dann mit MysqlCommand.Prepare() in die Datenbank übertragen zum Parsen+Optimieren.
  5. dann in der Schleife für jede Zeile nur noch die Parameterwerte setzen und MysqlCommand.ExecuteNonQuery ausführen. Jetzt werden nur noch die Daten übertragen. Das sollte schnell sein weil das Statement nicht jedes Mal neu geparst werden muß.
  6. Am Ende Transaktion committen.

Teste das erst einmal. Sollte das zu langsam sein, dann evtl mit Bulk Inserts beschäftigen. Bei pallelem Einfügen solltest Du erst einmal prüfen wo der Flaschenhals sich befindet. Außerdem müsstest Du wahrscheinlich für jede parallele Task ein eigenes DB-Connection-Objekt verwenden, der Transaktionsschutz geht dann flöten.

16.828 Beiträge seit 2008
vor 10 Jahren

Eigentlich ist die Anforderung von CSharpFreak eine völlig klare Sache für einen Producer/Consumer-Pattern und zum Beispiel TPL-Pipelines.
Die Umgebungsinformation hat hier gefehlt; für DB-Sache selbst sollte man aber eben kein TPL nutzen. Für den Gesamtprozess schon.

**Vorgehen:**1. Collection<String> urls: hier sind die URLs die abgefragt werden sollen. Wie diese in die Collection kommen: viele Wege führen nach Rom.

  1. Viele Tasks (WebTask) kümmern sich nun um die HTTP WebRequests (Achtung: Windows Betriebssysteme im Standard machen bei 10 Schluss, also reichen hier i.d.R. 10 Tasks).
  2. Collection<HttpResult> webResuls: hier legen die WebTasks ihre Ergebnisse rein
  3. Viele Tasks (DbTasks) holen sich nun die Ergebnisse aus webResuls und pushen die Daten in die Datenbank (lieber mit weniger Anfangen und langsam steigern. Irgendwann kommt der Punkt wo mehr Tasks das gesamte System verlangsamen)

Wichtig: jeder DbTasks hat seine eigene Datenbankverbindung.

Vorteil der TPL Pipeline ist die absolute Kapselung der einzelnen Aufgaben sowie die sehr einfache horizontale und vertikale Skalierung.

Anmerkung:
Collection<HttpResult> könnte auch ein Verarbeitungspaket sein in dem X Datenbank-Aufgaben drin liegen, die dann mit einer Transaktion verarbeitet werden.

C
CSharpFreak Themenstarter:in
42 Beiträge seit 2014
vor 10 Jahren

Danke - habe es weitestgehend versucht so zu implementieren und läuft soweit auch ganz okay.
Jedoch mit dem Resultat das der MySqlBulkLoader bei der load-Methode oft eine exeption wirft.

Fehlermeldung:
MySql.Data.MySqlClient.MySqlException (0x80004005): Deadlock found when trying to get lock; try restarting transaction

Jede Task hat seine eigene MySQL-Verbindung.

16.828 Beiträge seit 2008
vor 10 Jahren

Bist Dir sicher? Die Fehlermeldung deutet auf ein Problem mit den Tasks/Threads hin.

C
CSharpFreak Themenstarter:in
42 Beiträge seit 2014
vor 10 Jahren

Ja ich bin mir sicher aber habe gerade das hier gelesen
MDSN SqlBulkCopy

Any public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.

Heisst für mich das die Klasse/Instanz zwar Thread-safe ist aber nicht wenn ich mehrere davon erstelle... daraus schließe ich dann auch das ich effektiv nur eine Verbindung benötige da ich sowieso nicht Parallel einfügen kann?

Oder es gibt irgendeine Einstellung am MySql-Server wo ich etwas einstellen kann das es Funtkioniert?

16.828 Beiträge seit 2008
vor 10 Jahren

Statische Methoden, die keine Ressourcen teilen, sind immer Thread-Safe.
Es steht aber dran, dass Instanzen einer Klasse eben NICHT garantiert Thread-safe sind.

Du solltest Dir mal durchlesen, was Thread-Safe heisst. Denke Dir fehlen immer noch die Grundlagen von OOP und Thread-Handling. [Artikel] Multi-Threaded Programmierung

C
CSharpFreak Themenstarter:in
42 Beiträge seit 2014
vor 10 Jahren

Danke werde ich mir heute mal durchlesen - Wobei ich eig. weiß was es sein sollte.

Habe mich allerdings auch vertan ich benutze MySqlBulkLoader mit der load-Methode.

Da SqlBulkCopy für MS Datenbanken ist und nicht für MySql 😦