Laden...

Wie mehrere HttpWebRequest Parallel durchführen ?

Erstellt von micha0827 vor 6 Jahren Letzter Beitrag vor 6 Jahren 6.818 Views
M
micha0827 Themenstarter:in
85 Beiträge seit 2015
vor 6 Jahren
Wie mehrere HttpWebRequest Parallel durchführen ?

Guten Tag,

Ich rufe von einem Dienst per API Daten in XML Form ab. Der Dienst liefert pro Abruf 100 Datensätze. Dazu habe ich den HttpWebRequest in eine Methode gepackt und rufe die Methode in einer Schleife auf und packe alles in ein Dataset, mit dem ich dann weiterarbeite wenn alle Daten abgerufen sind. Leider können das Insgesamt schonmal 30.000 Datensätze werden, also 300 Durchläufe. Leider hat Azure einen Timeout den ich nicht ändern kann. Die Abfrage mit den 30.000 Sätzen braucht z.B. rund 6min und endet in einem Timeout.

Der Dienstanbieter lässt 18 gleichzeitige Abrufe zu. Leider weiss ich nicht wie ich die Methode 18x gleichzeitig aufrufe. Auch weiss ich nicht ob ich dann unterschiedliche Rückgabevariablen brauche, aktuell habe ich nur 1 weil die ja immer schön nach der Reihe wieder leer gemacht wird.

for (int z = 0; z < 300; z++)
{
    response = CallApi(Parameter);
    DatatableFuellen(response);
}

Bin offen für jeden Vorschlag
Danke Michael

T
2.219 Beiträge seit 2008
vor 6 Jahren

Schau dir mal Parallel.ForEach oder System.Threading.Tasks an.
Parallel.ForEach dürfte sogar der bessere Ansatz für dich sein.
Je nachdem wie dein kompletter Code aussieht, musst du ggf. umbauen.
Aber das solltest du selbst mal testen.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

M
micha0827 Themenstarter:in
85 Beiträge seit 2015
vor 6 Jahren

Davon hab ich noch gar nichts gelesen. Klingt aber interessant. Async Await ist nichts für mein Problem ?

Michael

16.807 Beiträge seit 2008
vor 6 Jahren

Async hat nichts mit Parallelität zutun. Nein, hilft Dir nicht.

D
985 Beiträge seit 2014
vor 6 Jahren

Warum wartest du mit dem Befüllen der DataTable nicht, bis du alle Daten (meinetwegen auch parallel) geladen hast?

Das geht dann mit PLINQ

class Program
{
    static void Main( string[] args )
    {
        var query = Enumerable.Range( 0, 300 )
            .Select( e => new { Index = e, Parameter = e } )
            .AsParallel()
            .WithDegreeOfParallelism( 18 )
            .Select( e => new { e.Index, Result = CallApi( e.Parameter ) } )
            .OrderBy( e => e.Index )
            .SelectMany( e => e.Result );

        var result = query.ToList();

    }

    static IList<int> CallApi( int parameter )
    {
        Thread.Sleep( 500 );
        return Enumerable.Range( parameter * 100, 100 ).ToList();
    }
}

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo micha0827,

Async Await ist nichts für mein Problem ?

das wäre genau das passende, da es sich bei WebRequests um IO-Vorgänge handelt. Hier mit Parallel.Foreach, PLinq etc. zu arbeiten ist suboptimal, da CPU-Threads mit Warten blockiert werden.

Hier sollten 18 asynchrone Vorgänge gestartet werden, wenn einer davon fertig ist, der nächste Vorgang, usw. bis alle 300 durch sind. Grob mit Code veranschaulicht so


public async Task<IList<string>> DownloadDataAsync(string url, int count, int maxParallelRequests = 18)
{
    using (HttpClient httpClient = new HttpClient())
    {
        List<Task<string>> taskList = new List<Task<string>>(maxParallelRequests);
        List<string> resultList     = new List<string>(count);

        for (int i = 0; i < count; ++i)
        {
            var downloadTask = httpClient.GetStringAsync(url);
            taskList.Add(downloadTask);

            // Wenn maxParallelRequests keine neuen Requests mehr starten
            // stattdessen "asynchron warten" bis ein Request fertig ist und dann einen neuen starten
            // Dadurch sind immer nur maximal maxParallelRequests Requests vorhanden
            if (taskList.Count >= maxParallelRequests)  // == würde reichen, aber so ist es auf Nummer sicher
            {
                var completedTask = await Task.WhenAny(taskList).ConfigureAwait(false);
                taskList.Remove(completedTask);
                resultList.Add(await completedTask);    // Task sicher fertig -> kein ConfigureAwait(false) nötig
            }
        }

        await Task.WhenAll(taskList).ConfigureAwait(false);

        foreach (var task in taskList)
            resultList.Add(await task);                 // Task sicher fertig -> kein ConfigureAwait(false) nötig

        return resultList;
    }
}

Das zeigt nur das Grundprinzip, lässt sich beliebig abwandeln und auch kann ganz anders (mit TaskCompletionSource) umgesetzt werden damit das ganze Infrastrukturrauschen raus fällt. Aber das geht hier wohl zu weit, denn für das Verständnis sollte dieser Code reichen.

Edit: siehe Throttling -- Begrenzung der max. Anzahl an gleichzeitig ausgeführten asynchronen Vorgängen

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

16.807 Beiträge seit 2008
vor 6 Jahren

Für die Parallelität der 18 Verbindungen ist aber nicht async verantwortlich, sondern TPL.
Daher hilft ihm async beim Grundproblem eben nicht.

Zusätzlich hilft async natürlich, dass die Gesamtoperation nicht blockiert.

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo Abt,

es stimmt dass async/await nicht für die Parallelität verantwortlich ist, da geht es "nur" um asynchrone Vorgänge.

Hier per TPL mehrere (CPU-) Threads zu verwenden, die nichts anderes tun als zu warten bis die Response da ist, ist Verschwendung.

Mit gezeigtem Code wir nur 1 Thread verwendet, der Rest passiert via IO-Abschlussthreads.
Dieser eine Thread startet 18 Vorgänge, wartet bis eine Response da ist und startet den nächsten Thread. Das ist ressourcenschonend. Sollte dieser Code in einer Server-Anwendung verwendet werden, so skaliert diese Variante um etliches besser als eine TPL-Variante.

Für die Parallelität der 18 Verbindungen ist aber nicht async verantwortlich

Im gezeigten Code ist für die Parallelität der 18 Verbindungen die for-Schleife verantwortlich. Dazu ist keine TPL nötig.
BTW: der OT hat gefragt ob async/await nichts für sein Problem ist und das ist sehr wohl.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

D
985 Beiträge seit 2014
vor 6 Jahren

@gfoidl

~~Wenn nur ein Thread dafür verwendet wird, dann müsste man diese Tatsache ja über den TaskManager sehen können. Wenn der entsprechende Code-Teil startet, dann sollte die Anzahl der Threads zu diesem Prozess um 1 ansteigen.

Bei mir steigt die Anzahl der Threads aber exakt um 16 (von 6 auf 22)

Schaue ich jetzt an der falschen Stelle oder wo ist da mein Problem?~~

Quark, daran kann man gar nichts ablesen, der Wert springt immer auf die 22 sobald die TPL anfängt zu rödeln 😁

16.807 Beiträge seit 2008
vor 6 Jahren

Die TPL im Hintergrund erkennt, dass Tasks verwendet werden und bereitet in seiner Queues viele Threads vor.
Ob sie tatsächlich verwendet werden oder nicht, entscheidetet der Scheduler.

Aber durch das Wrappen der Tasks von Async kann gar nicht nur ein Thread verwendet werden.
HttpClient is Thread Safe und muss bei Async-Methodenen einen eigenen Task verwenden; vermutlich mit Task.Run, was automatisch einen eigenen Thread erzeugt.
Ich bezweifle, dass die Gesamtlösung ressourcensparender ist.

Trotzdem ist die Lösung hübsch; das zweifle ich gar nicht an.

M
micha0827 Themenstarter:in
85 Beiträge seit 2015
vor 6 Jahren

Also zumindest ist die Abfrage mit 30.000 Datensätzen von ca. 6 min auf ca. 1 min runtergegangen bei der Verwendung mit Parallel.For

Interessieren würde mich an der Stelle die Überwachungsmöglichkeit. Das Task Fenster in VS2017 zeigt nichts an. Allein im Windows Task Manager Netzwerküberwachung habe ich ab und zu den iisstart Prozess 2x drin mit der API Adresse. Heisst das, das er dann 2 Calls gleichzeitig macht ?

Habe jetzt auch eine Abfrage drin mit 280.000 Sätzen in 2:30 min ... Vielleicht alles noch nicht optimal, aber zumindest das aktuelle Problem gelöst.

Ein komisches Phenomen ist beim Testen aufgetaucht. Wenn ich bei der Ausgabe der Anzahl der Datensätze ein Tausender Trennzeichen einfüge mit string.format geht die 30.000er Abfrage von 1 min auf knapp 2 min hoch ... warum auch immer.

Michael

16.807 Beiträge seit 2008
vor 6 Jahren

Exemplarische Vorgehensweise: Debuggen einer parallelen Anwendung

TPL wird niemals zwei Prozesse Deiner Anwendung starten. Prozesse stellen eine andere Isolationsebene dar als Tasks oder Threads.
Das Thema mit iistart wird eher nicht von dem Code Umbau kommen.

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo Sir Rufo,

auch wenns jetzt zu spät ist, aber was der Task-Manager genau an Threads anzeigt weiß ich nicht und ganz traue ich dem sowieso nicht. perfom.exe ist da schon besser.
Hier spielen aber auch ThreadPool, TPL eine Rolle. windbg wäre da wohl am genauesten.
Mit dem VS-Debugger hab ich auch gesehen, dass etliche Threads verwendet werden -> aber intern ausgehend vom HttpClient für das Runterdelegieren zum Socket. Komisch dass diese so gezählt werden, denn ich hätte das als IO-Threads abgetan. Insofern war meine Aussage mit einem Thread wohl zu stark vereinfacht bzw. zu idealiisert.

Aber bevor ich dazu weitere Ausführungen selbst schreibe verweise ich diesbezüglich auf Async in depth | Deeper Dive into Tasks for an I/O-Bound Operation und auf There Is No Thread

Hallo Abt,

Aber durch das Wrappen der Tasks von Async kann gar nicht nur ein Thread verwendet werden.

Genau -- wenns rein IO ist, so wird gar kein Thread verwendet 😉
Und hier gehe ich wegem dem WebRequest von IO aus.
Hier wird vom OS für die IO-Aktion nur ein Task zurückgegeben, damit das für .net "ins Bild" passt und mit bestehenden Konstrukten abgedeckt werden kann. Aber das geht zurück auf die Begin/End-Methoden in .net 1. Task ist da nur wesentlich einfacher und netter zum Arbeiten.

Wie der HttpClient genau aufgebaut ist hab ich mir nicht angeschaut (auch wenns open source ist).
Dass ein Task zurückkommt ist klar, aber dass dafür Task.Run verwendet wird wäre kontraproduktiv bei IO-Operationen, da dort erst recht geblockt werden müsste. Das ist letztlich auf OS-Kernel-Ebene und da ist IO asynchron. Das wird einfach weitergereicht.

Ich behaupte dass die Gesamtlösung ressourcensparender ist. Alleine schon deshalb das weniger Threads aktiv beim Werken sind und weniger Objekte für WaitHandles, etc. erstellt werden müssen (GC Pressure).

Wie oben schon erwähnt sind durch das asynchrone IO v.a. Server bereit für mehr Durchsatz und skalieren besser.
Bei reiner Client-Anwendung ist es eher nur weniger GC Pressure und der kann od. auch nicht entscheidend sein.

Trotzdem ist die Lösung hübsch

Danke 😃

Edit: Hab mir eben den HttpClient angeschaut. Die relevante Gaudi spiel sich in den Implementierungen von HttpClientHandler ab, dabei gibt es einen für Windows, Unix und .net 4.6

In Unix und Windows ist das alles rein asynchron, ohne Task.Run bzw. ohne Task.Factory.StartNew

Windows: return _winHttpHandler.SendAsync(request, cancellationToken);
Unix: return _curlHandler.SendAsync(request, cancellationToken);

In .net 4.6 ist das (interessanterweise) anders:


                // BeginGetResponse/BeginGetRequestStream have a lot of setup work to do before becoming async
                // (proxy, dns, connection pooling, etc).  Run these on a separate thread.
                // Do not provide a cancellation token; if this helper task could be canceled before starting then
                // nobody would complete the tcs.
                Task.Factory.StartNew(_startRequest, state);

Warum, weiß ich nicht hab ich nicht weiter evaluiert.

Der Kommentar bei Unix unter dem CurlHandler ist auch ganz interessant.

Zitat von: corefx/CurlHandler.cs at master
When a request is made to the CurlHandler, callbacks are registered with libcurl, including state that will
be passed back into managed code ...
For the beginning phase of the request, the native code may be the only thing referencing the managed objects, since
when a caller invokes "Task<HttpResponseMessage> SendAsync(...)", there's nothing handed back to the caller that represents
the request until at least the HTTP response headers are received and the returned Task is completed with the response
message object. ...

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

M
micha0827 Themenstarter:in
85 Beiträge seit 2015
vor 6 Jahren

Guten Tag,

nun ist doch wieder ein Problem aufgetaucht. Ich rufe mit dem Parallel For eine Methode auf, die einen JSON String per API abruft, diesen deserialisiert und in eine Datatable schreibt.

Nun bekomme ich ab und zu eine Exeption mit der Meldung dass der Index der dt defekt ist. Habt Ihr noch einen Tip wo ich auf die Fehlersuche gehen kann ?

Michael

D
985 Beiträge seit 2014
vor 6 Jahren

Wenn ich das so lese, dann vermute ich mal

Zitat von: DataTable-Klasse (System.Data)
This type is safe for multithreaded read operations. You must synchronize any write operations.

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo micha0827,

wie Sir Rufo schon zitiert hast musst du die Zugriffe auf das DataTable synchronisieren (am einfachsten per lock).

Oder du verwendest die Überladung von Parallel.For bzw. Parallel.ForEach die local-Init hat, denn damit kannst du für jeden Thread ein DataTable haben, dann ist diese DataTable-Instanz "sicher", da keine anderen Threads darauf zugreifen. Dafür sind es halt mehrere Instanzen und die Synchronisation wird in die Datenbank "verlagert".

Oder du nimmst die von mir vorgeschlagene Lösung, bei der die Zugriffe auf das (eine) DataTable synchron wären, da immer nur ein Thread (nach dem anderen) darauf zugreift.
Diese Lösung hat also mehrere Vorteile und ich weiß nicht warum du immer noch mit Parallel.For herumkämpfst und von Problem zu Problem irrst, wenns doch anders viel einfacher wäre.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

M
micha0827 Themenstarter:in
85 Beiträge seit 2015
vor 6 Jahren

Hallo gfoidl,

deine Variante ist für einen Anfänger schwer umzusetzen 😃 Deswegen habe ich es bis jetzt nicht so gemacht. Und das Parallel For hat auf Anhieb funktioniert.

Michael

16.807 Beiträge seit 2008
vor 6 Jahren

Das wäre aber halt nun mal die richtige Vorgehensweise und die eigentliche Lösung; Anfänger hin oder her.
Dass man es nicht kann, ist keine zu akzeptierende Ausrede. Muss man es sich halt aneignen und evtl. nen paar Tage investieren.

M
micha0827 Themenstarter:in
85 Beiträge seit 2015
vor 6 Jahren

Danke, der Code von gfoidl funktioniert super. 2 Fragen hätte ich noch dazu:

  1. ich möchte ja das Ergebnis auswerten. Ich muss im Komplettergebnis einen String finden. Dazu habe ich bis jetzt das Ergebnis deserialisiert in eine Datatable geschrieben, alles nacheinander und danach die Datatable nach dem String per Schleife durchsucht. Dann kam ja ab und zu der Fehler dass mein Datatable einen defekten Index hat. Kann ich überhaupt gleichzeitig in ein datatable schreiben ? Sollte ich dann das Ergebnisarray per Schleife durchlaufen oder macht das Sinn die DeSerialisierung schon in den AbrufTasks zu machen ?

  2. eine komische Sache ist aufgetaucht. Wenn ich normal 100 Abrufe mache brauche ich ca. 1:20 min, dann wollte ich Fiddler benutzen um zu überwachen .... er ist nach 16sec fertig ... mache ich Fiddler wieder zu .... wieder rung 1:20min ???

Danke
Michael

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo micha0827,

ad 1: wozu brauchst du überhaupt das DataTable? Eine List<string> reicht dazu nicht?

Kann ich überhaupt gleichzeitig in ein datatable schreiben ?

Nein, da DataTable nicht threadsicher ist.

  
resultList.Add(await completedTask);  
  

Hier ist die List<T> auch nicht threadsicher, aber die (schreibenden) Zugriffe erfolgen nacheinander (nicht zwingen vom selben Thread aus) und somit gibt es kein Problem mit einer potentiellen Race-Condition (welche dir den Index-Fehler gegeben hat).

In meinem Beispiel oben gibt die Methode eine List<string> zurück und wenn du diese hast, so kannst du die Auswertung erledigen.

Sollte ich dann das Ergebnisarray per Schleife durchlaufen oder macht das Sinn die DeSerialisierung schon in den AbrufTasks zu machen ?

Wie immer kann so eine Aussage nicht pauschal in die eine oder andere Richtung beantwortet werden, da letztlich nur Profilierung eine klare Aussage treffen kann.
Sollte die Deserialisierung aufwändig sein, so kann diese ebenfalls asynchron in Form eine Pipelinings durchgeführt werden. Aber nicht im AbrufTask, denn der soll -- wie der Name schon sagt -- nur "abrufen" und nichts weiter machen.
Im Beispiel-Code oben könnte das dann in etwa so eingebaut werden:


// statt
//var downloadTask = httpClient.GetStringAsync(url);
var downloadAndDeserializeTask = this.DownloadAndDeserializeAsync(url);

...

private async Task<string> DownloadAndDeserializeAsync(string url)
{
    string content = await _httpClient.GetStringAsync(url).ConfigureAwait(false);
    return await _serializer.DeserializeAsync(content).ConfigureAwait(false);
}

Anmerkungen: * Hier hab ich httpClient einfach so referenziert, obwohl aus dem Code nicht ersichtlich ist wie er bekannt gemacht wurde. Entweder du machst aus httpClient eine Instanz-Variable (= Feld) od. übergibts ihn als Argument an die Methode.

  • Die Deserialisierung findet in einer eigenen Serializer-Klasse statt, welche im Code mit _serializer verwendet wird. Das ist eine eigene Aufgabe, daher eine eigene Klasse.
  • Der Methodenname hat ein "And" drinnen und das ist oft ein code smell das impliziert, dass eine Methode zu viel tut und das single responsibility principle (SRP) verletzt. Diese Verletzung ist hier nicht der Fall, aber die Methode könnte auch anders benamt werden wie z.B. "ProcessUrl" o.ä. Wie es halt zu deinem Kontext besser passt.

ad 2: komisch, aber ich kenne deine Umgebung nicht um darüber treffende Aussagen zu machen. Hast du sonst vllt. einen (HTTP-) Proxy am laufen, der dann durch Fiddler umgangen wird?

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

M
micha0827 Themenstarter:in
85 Beiträge seit 2015
vor 6 Jahren

Ich hab das jetzt wie folgt angepasst um die Deserialisierten Daten zu erhalten:


public async Task<IList<string[,]>> DownloadDataAsync(string url, int count, int maxParallelRequests = 18)
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();

            List<Task<string[,]>> taskList = new List<Task<string[,]>>(maxParallelRequests);
            List<string[,]> resultList = new List<string[,]>(count);
            
            for (int i = 0; i < count; ++i)
            {
                var downloadAndDeserializeTask = this.DownloadAndDeserializeAsync(url,count);
                taskList.Add(downloadAndDeserializeTask);

                // Wenn maxParallelRequests keine neuen Requests mehr starten
                // stattdessen "asynchron warten" bis ein Request fertig ist und dann einen neuen starten
                // Dadurch sind immer nur maximal maxParallelRequests Requests vorhanden
                if (taskList.Count >= maxParallelRequests)  // == würde reichen, aber so ist es auf Nummer sicher
                {
                    var completedTask = await Task.WhenAny(taskList).ConfigureAwait(false);
                    taskList.Remove(completedTask);
                    resultList.Add(await completedTask);    // Task sicher fertig -> kein ConfigureAwait(false) nötig
                }
            }
            await Task.WhenAll(taskList).ConfigureAwait(false);

            foreach (var task in taskList)
            resultList.Add(await task);                 // Task sicher fertig -> kein ConfigureAwait(false) nötig

            watch.Stop();
            Console.WriteLine(watch.Elapsed);

            return resultList;
        }

private async Task<string[,]> DownloadAndDeserializeAsync(string url, int page)
        {
            using (HttpClient httpClient = new HttpClient())
            {
                string content = await httpClient.GetStringAsync(url).ConfigureAwait(false);
                Rootobject listingitem = JsonConvert.DeserializeObject<Rootobject>(content);
                int anzahl_items = listingitem.findItemsByKeywordsResponse[0].searchResult[0].item.Length;

                string[ , ] arr = new string[anzahl_items,2];

                for (int i = 0; i < anzahl_items; i++)
                {
                    arr[i,1] = listingitem.findItemsByKeywordsResponse[0].searchResult[0].item[i].itemId[0];
                    arr[i,2] = page.ToString();
                }
                return arr;
            }
        }


Der Aufruf der Methode:



static void Main(string[] args)
        {
            string url = "http://meineApi";
            int count = 10;

            ApiCall apicall = new ApiCall();
            
            Task t = apicall.DownloadDataAsync(url, count);
            t.Wait();

            
        }

bringt mir aber eine NullReferenceException. Wo muss ich denn da noch welches Objekt erstellen ?

Michael

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo micha0827,

[Artikel] Debugger: Wie verwende ich den von Visual Studio? , [FAQ] NullReferenceException: Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt
Bitte selber schauen, das sollte schon möglich sein.

HttpClient ist threadsicher und es wird auch empfohlen nur eine Instanz davon zu verwenden, da sonst immer eine eigene Socket-Verbindung auf-/und abgebaut werden muss. Das ist nicht nötig, v.a. wenn die Base-Adress die gleiche ist.

Ich überarbeite mal deinen Code und poste dann meine Lösung dazu.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo micha0827,

angehängt eine Lösung -- zwar nur beispielhaft -- wie das gelöst werden könnte bzw. wie ich das lösen würde.

Anmerkungen dazu:* ich hab ein paar C# 7 Features wie (value) tuples und throw expressions verwendet, das lässt sich aber auch leicht anders lösen und da es eh nur Beispiel ist stellt dies kein Problem dar

  • zum Download hab ich nur zufällige Namen verwendet die gegoogelt werden
  • zum Deserialisierren hab ich einen Dummy-Methode verwendet die nur den Inhalt als String (wieder) zurückgibt

Ich bin mir aber sicher, dass du es schaffst dies auf deine Aufgabenstellung umzubauen 😃

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

M
micha0827 Themenstarter:in
85 Beiträge seit 2015
vor 6 Jahren

hallo gfoidl,

bin immer noch dran an deinem Beispiel. Darf ich das denn so verstehen, dass alle Vorgänge in einem Thread nacheinander abgearbeitet werden ?

Konkret, wenn ich in einem Thread 2 Aufrufe:


Thread[] threads = new Thread[20];
threads[x] = new Thread(() => {
firstPage = apifindingcall.GetFirstPage(firsturl).GetAwaiter().GetResult();
lastPage = apifindingcall.GetFirstPage(lasturl).GetAwaiter().GetResult();
});
threads[x].Start();

habe, dann werden die 2 Methoden immer nacheinander ausgeführt in 50 Threads die parallel laufen ? Kann ich dann in diesem Thread das Ergebnis nach den 2 Methodenaufrufen in eine DB schreiben ?

Momentan versuche ich einen (oder mehrere) Deadlocks zu finden. 100 Threads laufen gut, bei 200 kommen dann die Ausnahmen.

Was wäre denn ein Szenario wenn ich 1000 Threads brauche, die 6 min insgesamt laufen, ich aber alle 5min einen Abgleich machen möchte ? Macht man dann wieder eine Skalierung auf 100 Threads a 10 Durchläufe ?

Michael

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo micha0827,

vergiss bitte umgehend den Code, den du im letzten Beitrag gepostet hast. Das ist ein Rückschritt. Nicht nur weil du von der Abstraktion die Tasks bringen zurück auf Threads gehst, sondern v.a. weil das keinen Sinn ergibt. Tu dir selbst einen Gefallen und schau dir die Grundlagen von asynchroner Programmierung an bevor du sie verwendest. Das Problem mit den Deadlocks ist hausgemacht, da eben die Grundlagen dazu fehlen und das Thema nicht so trivial wie eine rein synchrone Anwendung ist.
Genauso ist es einfach falsch hier mit einer parallelen Schleife od. PLinq irgendetwas zu versuchen.

Wenn du dich damit nicht auskennst, so mach es rein synchron, denn das ist noch besser als eine Variante die so halbwegs funktioniert und Deadlocks, Race Conditions, False Writes, etc. liefert.

Darf ich das denn so verstehen, dass alle Vorgänge in einem Thread nacheinander abgearbeitet werden ?

Lass uns zuerst betrachten was bei einem asynchronen Vorgang am Beispiel des (asynchronen) Downloads eines Strings passiert wie in httpClient.GetStringAsync. Ich schreib dabei auch die Threads dazu, auch wenn durch Tasks das alles wegabstrahiert wird.

  1. Dein Code läuft in einem Thread A -- jeder Code läuft in einem Thread, eine Prozess besteht aus mindestens einem Thread -- und ruft httpClient.GetStringAsync auf
  2. Der HttpClient bereitet alles für das Downloaden vor -- läuft immer noch in Thread A und zwar synchron zum deinem Aufruf
  3. Der HttpClient startet (via Betriebssystem od. was auch immer, das ist hier egal) den asynchronen Download -- damit dein Code eben nicht Warten muss bis der Download fertig ist passiert das asynchron. Damit dein Code aber mitbekommt, wann das Resultat vorhanden ist wird ein Task zurückgegeben
  4. Dein Code befindet sich immer noch in Thread A und kann andere Aufgaben erledigen. Der asynchrone Download läuft dabei irgendwo anders -- wo spielt hier für diese Betrachtung keine Rolle, siehe obige Links wenn du (später dann) mehr davon wissen willst.
  5. Irgendwann ist der Download fertig und das Betriebssystem od. was auch immer meldet dem HttpClient dass die Daten da sind. Nun übernimmt der HttpClient in Thread B (od. sonst irgendeinem Thread aus dem ThreadPool -- exakter aus den IO-Abschlussthreadpool) die Arbeit und bereitet das Ergebnis auf.
  6. Der HttpClient in Thread B schreibt das Ergebnis in den Task, den er vorher deinem Code zurückgegeben hat und Thread B ist dann fertig.
  7. Dem Task wurde eine Continuation (= Folgeaktion) angehängt, das hat der Compiler gemacht durch die Magie die er bei async/await anwendet, und diese Folgeaktion wird jetzt in Thread C ausgeführt*
  8. In Thread C, welcher die Continuation zu dem Task ausführt, kann mit dem Ergebnis des Downloads gearbeitet werden.

* ist eine grobe Vereinfachung, denn in Wirklichkeit hängt es vom Context der Anwendung ab, aber dieses Detail würde hier jetzt zuweit gehen

So nun betrachten wir den Download von vielen Strings, wobei max. 18 gleichzeitig durchgeführt werden sollen -- ganz entsprechend meinem oben angehängten Beispielcode.

Die Implementierungsdetails von Task.Factory.Throttling können dir an dieser Stelle egal sein, betrachte das einfach als gegeben und dass der Code dort schon das richtige macht (ist ja von mir in den Snippets gepostet worden 😄)

Betrachtet wird hier v.a. die Methode Processor.ProcessUrls.
In taskFuncs wird ein Func-Delegat definiert, der pro herunterzuladender Url eine (anonyme) Methode zum Starten des Downloads hat. Das ist wichtig, denn die Downloads werden da noch nicht gestartet, sondern nur die Func zum Starten des Downloads für eine Url erstellt.

Was macht Task.Factory.Throttling konzeptionell?
Das ist für das Verständnis wichtig, die Details können dir wie vorhin erwähnt egal sein. Die Erklärung ist hier auch vereinfacht, da es sonst zu detailliert sein würde.1. Es wird eine Liste mit Tasks erstellt, entsprechend der Anzahl an Urls die heruntergeladen werden sollen.

  1. Durch die übergebenen Func-Delegaten (siehe vorhin) werden maxParallelRequests Downloads gestartet. Dabei passiert folgendes (genau wie oben beschrieben):
    * Thread A ruft httpClient.DownloadStringAsync auf
    * HttpClient bereitet den Download vor und gibt einen Task zurück
    * Der Download findet asynchron statt und wird irgendwann fertig werden

  2. Wir sind immer noch in Thread A. Jedem Task den der HttpClient zurückgibt wird eine Continuation angehängt, die dann ausgeführt wird wenn der Download dann irgendwann fertig ist.

  3. Sind alle maxParallelRequests Downloads gestartet diese Methode fertig und gibt die eingangs erstellte Liste an Tasks an die ProcessUrls-Methode zurück

  4. Über diese Liste an Tasks wird in der foreach-Schleife iteriert. Da die Tasks aber noch kein Ergebnis haben, d.h. der Download der jeweiligen Url noch nicht fertig ist, wird dem Compiler mitgeteilt dass er bei await eine Continuation anhängen soll, die dann ausgeführt wenn der Task fertig ist. Es sind also jede Menge Continuations im Spiel, die ausgeführt werden sobald ein Download fertig ist.

  5. So nun ist ein Download fertig.
    * In Task.Factory.Throttling wird die Continuation ausgeführt. Dabei das Ergebnis in den an ProcessUrls zurückgegeben Task geschrieben und weiters wird ein neuer Download gestartet wie unter 2. Ein fertiger Download startet einen neuen Download, so bleiben es maxParallelRequests Downloads und nicht mehr (sondern dann weniger, wenn keine neue mehr gestartet werden können, da das Ende der Liste hergeht)
    * In ProcessUrl wird die Continuation ausgeführt, dabei kann das Ergebnis verwendet werden.
    * foreach iteriert weiter und der nächste Task, der für den nächsten Download steht, hat noch kein Ergebnis, also wird die Continuation angehängt und das ganze wieder von vorne. Ist ein weiterer Download fertig, so wird die Continuation ausgeführt, usw. usf. bis alle Downloads fertig sind.

Das wesentliche ist also, dass durch die foreach-Schleife, dem await in der Schleife und Task.Factory.Throttling immer nur ein fertiger Task (~ fertiger Download) abgearbeitet wird. Dadurch können die Ergebnisse nacheinander verarbeitet werden und es treten an dieser Stelle keine Race Conditions und auch keine Deadlocks auf. Ein Zugriff auf eine gemeinsame Ressource (wie ein DataTable) ist problemlos möglich, da eben keine gleichzeitigen Zugriffe stattfinden. Durch diesen Code werden zwar die Downloads "gleichzeitig" ausgeführt, die Ergebnisse aber nacheinandern -- so wie sie fertig werden -- abgearbeitet.

So ich hab jetzt versucht das möglichst einfach und ein wenig ausführlicher zu erklären. Wenn du dazu noch Fragen hast, so lies dich bitte vorher in das Thema ein und bevor wir uns im Kreis zu drehen beginnen werde ich aus diesem Thema aussteigen. Daher auch vorsorglich der Hinweis zu [Hinweis] Wie poste ich richtig? Punkt 1, denn in diesem Thema wurde dir nun wirklich genug Hilfe geboten.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

M
micha0827 Themenstarter:in
85 Beiträge seit 2015
vor 6 Jahren

Hallo gfoidl,

Vielen Dank dass du dir die Mühe gemacht hast dein Beispiel zu erläutern. Auch hast du Recht damit dass das Thema Multitasking noch sehr schwer für mich ist. Und natürlich muss man sich auch einlesen. Aber das ist bei manchen Themen nicht so einfach. Ich kann zu Google nicht sagen "Lass alles weg was nicht mehr aktuell ist" und wenn ich mir meine 1500 Seiten Bücher nehme dann gibt es da 28 Seiten über TPL mit Beispielen naja ...

Ich muss allerdings zu meiner Verteidigung sagen, dass meine Codezeilen nicht dazu gedacht waren dein Beispiel zu verändern, sondern mir fehlt zu deinem Beispiel noch eine obere Ebene.

Das bedeutet ich habe nicht eine Liste mit URLs sondern mehrere Listen, die natürlich auch parallel abgearbeitet werden sollen.

Nochmals Vielen Dank
Michael

D
985 Beiträge seit 2014
vor 6 Jahren

Das bedeutet ich habe nicht eine Liste mit URLs sondern mehrere Listen, die natürlich auch parallel abgearbeitet werden sollen.

Ok, wenn du eine Frage stellst, dann bitte nicht nur das linke Ohr vom Eisbären sondern den ganzen Bären (heißt das ganze Problem schildern).

Dieses winzige Detail kann - ganz im Sinne der Chaos-Theorie, wo der Schmetterling mit einem Flügelschlag einen Orkan auslöst - eben einen solchen Orkan auslösen und alle bisherigen Lösungsvorschläge ad absurdum führen, weil man dieses Szenario anders lösen muss.

Nun mach Butter bei die Fische und beschreibe dein gesamtes Szenario mit diesen Abfragen.

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo micha0827,

Sir Rufo hat da recht, aber wenn du das Problem neu beschreiben willst, so erstellt bitte ein neues Thema, sonst kennt sich hier keine mehr aus.

http://www.albahari.com/threading/ schau dir aber vorher an 😉

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"