2009-08-23 4 views
12

Derzeit habe ich eine große Anzahl von C# -Berechnungen (Methodenaufrufe), die sich in einer Warteschlange befinden, die nacheinander ausgeführt wird. Bei jeder Berechnung wird ein Dienst mit hoher Latenz verwendet (Netzwerk, Festplatte ...).Entwurfsmuster-Alternative zu Routinen

Ich wollte Mono-Coroutinen verwenden, damit die nächste Berechnung in der Berechnungswarteschlange fortgesetzt werden kann, während eine vorherige Berechnung auf die Rückkehr des Dienstes mit hoher Latenz wartet. Ich bevorzuge es jedoch, mich nicht auf Mono-Coroutinen zu verlassen.

Gibt es ein Entwurfsmuster, das in reinem C# implementiert werden kann, das es mir ermöglicht, zusätzliche Berechnungen zu verarbeiten, während ich auf die Rückkehr von Diensten mit hoher Latenzzeit warte?

Dank

Update:

Ich brauche eine große Zahl (> 10000) von Aufgaben auszuführen, und jede Aufgabe eines hohen Latenz-Dienst wird. Unter Windows können Sie nicht so viele Threads erstellen.

Update:

Grundsätzlich, ich brauche ein Design-Muster, das die Vorteile emuliert (wie folgt) von Tasklets in Stackless Python (http://www.stackless.com/)

  1. Huge # Aufgaben
  2. Wenn ein Task blockiert die nächste Aufgabe in der Warteschlange ausgeführt
  3. Kein verschwendet CPU-Zyklus
  4. Minimale Overhead-Switching zwischen Aufgaben
+0

Können Sie hier eine bessere Lösung für Korotinen als Lösung vorschlagen? Es scheint nach einem (ausgeglichenen) Threading zu fragen, wie in der Antwort von dtb. –

+0

Nun, ich muss eine große Anzahl (> 10000) von Aufgaben ausführen, und jede Aufgabe wird einen Dienst mit hoher Latenz verwenden. Unter Windows können Sie nicht so viele Threads erstellen. – jameszhao00

+0

Klingt wie ein Job für ThreadPool, +1 für jscharf –

Antwort

9

Sie können kooperatives Microthreading mit IEnumerable simulieren. Leider funktioniert dies nicht mit blockierenden APIs. Daher müssen Sie nach APIs suchen, die Sie abfragen können, oder über Callbacks, die Sie für die Signalisierung verwenden können.

Betrachten wir ein Verfahren

IEnumerable Thread() 
{ 
    //do some stuff 
    Foo(); 

    //co-operatively yield 
    yield null; 

    //do some more stuff 
    Bar(); 

    //sleep 2 seconds 
    yield new TimeSpan (2000); 
} 

Der C# -Compiler diese in eine Zustandsmaschine auspacken - aber das Aussehen ist das eines kooperativen Microthread.

Das Muster ist ziemlich einfach. Sie implementieren einen "Scheduler", der eine Liste aller aktiven IEnumeratoren führt. Beim Durchlaufen der Liste "läuft" jeder mit MoveNext(). Wenn der Wert von MoveNext falsch ist, ist der Thread beendet und der Scheduler entfernt ihn aus der Liste. Wenn dies der Fall ist, greift der Scheduler auf die Current-Eigenschaft zu, um den aktuellen Status des Threads zu ermitteln. Wenn es ein TimeSpan ist, möchte der Thread schlafen, und der Scheduler hat ihn in eine Warteschlange verschoben, die in die Hauptliste zurückgespült werden kann, wenn die Schlafzeiten abgelaufen sind.

Sie können andere Rückgabeobjekte verwenden, um andere Signalisierungsmechanismen zu implementieren. Definieren Sie beispielsweise eine Art von WaitHandle. Wenn der Thread einen von diesen ergibt, kann er in eine Warteschlange verschoben werden, bis der Handle signalisiert wird. Oder Sie können WaitAll unterstützen, indem Sie ein Array von Wait-Handles bereitstellen. Sie könnten sogar Prioritäten setzen.

Ich habe eine einfache Implementierung dieses Schedulers in etwa 150LOC gemacht, aber ich bin noch nicht dazu gekommen, den Code zu bloggen. Es war für unseren PhyreSharp PhyreEngine Wrapper (der nicht öffentlich sein wird), wo es ziemlich gut funktioniert, um ein paar hundert Charaktere in einem unserer Demos zu kontrollieren.Wir haben das Konzept von der Unity3D-Engine ausgeliehen - sie haben einige Online-Dokumente, die es aus der Sicht eines Anwenders erklären.

+0

Interessante Sachen. Ich habe mir das schon einmal angeschaut, aber ich glaube nicht, dass man Code, der mehr als 1 Level tief ist, richtig ertragen kann. I.e. Wenn Sie eine Coroutine-Startfunktion haben, die eine andere Funktion aufruft, die nachgibt, bricht die ganze Sache. – jameszhao00

+0

In den meisten Fällen können Sie einfach die Funktionen implementieren, die Sie als Ierumerables und Foreach + Yield aufrufen möchten, obwohl Sie natürlich ein wenig Indirection benötigen, um Rückgabewerte zu erhalten - ich kann mich nicht erinnern, ob Ref Params funktionieren mit einem Ertrag, aber * wenn * sie nicht, gibt es eine Reihe anderer Möglichkeiten. Sie könnten eine Referenz auf ein Objekt mit Feldern übergeben und diese verwenden, um Werte aus der Funktion zurückzuholen, oder Lambda-IDs übergeben, die Ihre Locals festlegen oder einen bestimmten Typ aus den errechneten Werten filtern können usw. –

+0

I.e. Thing zurückgegeben = null; foreach (var o in Funktion ((x) => returned = x)) Rendite o; –

6
+0

Ja, das das Problem der Fortsetzung der nächsten Berechnung bei einer latenzintensiven Operation nicht löst. Aufgabenparallelität erleichtert nur das parallele Rechnen. – jameszhao00

+4

die Taskparallelbibliothek kann mehr Threads als Cores verwenden, wenn also festgestellt wird, dass die verwendeten Threads nicht viel CPU-Zeit verbrauchen, kann sie mehr Tasks planen ... Dies kann zu übermäßigem IO führen und erfordert daher sorgfältiges Tuning hoffte, dass die Bibliothek viel davon für Sie tut, aber Benchmarking und Überprüfung ist immer eine gute Idee ... – ShuggyCoUk

+0

True Threads sind ein bisschen zu schwer für dieses Projekt. Ich brauche 20k-80k-Rechenaufgaben, die gleichzeitig ausgeführt werden. – jameszhao00

1

Ist das nicht eine herkömmliche Verwendung von Multi-Threaded wird bearbeitet?

Werfen Sie einen Blick auf Muster wie Reactor here

+0

Entschuldigung. Ich bin ein wenig verwirrt darüber, wie das hier verwendet werden kann. – jameszhao00

1

es Schreiben Async IO verwenden könnte ausreichend sein.

Dies kann zu nasy, schwer zu debuggen Code ohne starke Struktur im Design führen.

+0

Auf einer niedrigeren Schicht, ja werde ich AsyncIO verwenden, um Netzwerkpakete zu senden/empfangen. In den höheren Schichten werde ich jedoch eine Art von synchronem RPC implementieren. – jameszhao00

5

Ich würde empfehlen, die Thread Pool mit mehreren Aufgaben aus der Warteschlange zur Ausführung auf einmal in überschaubaren Chargen eine Liste der aktiven Aufgaben verwenden, die aus Feeds die Aufgabenwarteschlange.

In diesem Szenario würde Ihr Haupt-Worker-Thread zunächst N Tasks aus der Warteschlange in die Liste der aktiven Tasks verschieben, die an den Thread-Pool gesendet werden (höchstwahrscheinlich unter Verwendung von QueueUserWorkItem), wobei N für einen überschaubaren Betrag steht Thread-Pool, senken Sie Ihre App mit Thread-Planungs- und Synchronisierungskosten oder nutzen Sie den verfügbaren Speicher aufgrund des kombinierten E/A-Speicheraufwands für jede Aufgabe.

Wenn eine Aufgabe die Beendigung des Worker-Threads signalisiert, können Sie sie aus der Liste der aktiven Aufgaben entfernen und die nächste Aufgabe aus Ihrer Aufgabenwarteschlange hinzufügen.

Dadurch können Sie eine rollende Menge von N Aufgaben aus Ihrer Warteschlange haben. Sie können N manipulieren, um die Leistungsmerkmale zu beeinflussen und das beste für Ihre speziellen Umstände zu finden.

Da Sie letztlich von Hardware-Operationen (Festplatten-I/O und Netzwerk-I/O, CPU) Engpässe sind, stelle ich mir kleinere ist besser. Zwei Thread-Pool-Tasks, die auf Festplatten-E/A arbeiten, werden höchstwahrscheinlich nicht schneller ausgeführt als einer.

Sie können auch Flexibilität in Größe und Inhalt der aktiven Aufgabenliste implementieren, indem Sie sie auf eine bestimmte Anzahl bestimmter Aufgabenarten beschränken. Wenn Sie beispielsweise auf einem Computer mit vier Prozessorkernen arbeiten, stellen Sie möglicherweise fest, dass die leistungsstärkste Konfiguration aus vier CPU-gebundenen Tasks besteht, die gleichzeitig mit einer festplattengebundenen Task und einer Netzwerkaufgabe ausgeführt werden.

Wenn Sie bereits eine Task als Festplatten-E/A-Task klassifiziert haben, können Sie vor dem Hinzufügen einer anderen Festplatten-E/A-Task warten, bis sie abgeschlossen ist, und Sie können eine CPU-gebundene oder netzwerkgebundene Task planen inzwischen.

Hoffe das macht Sinn!

PS: Haben Sie Abhängigkeiten in der Reihenfolge der Aufgaben?

+0

Nein. Es gibt keine Anforderung in der Reihenfolge der Ausführung. – jameszhao00

+0

Lass mich sehen, ob ich das habe. Für jeden Kern befindet sich eine bestimmte Anzahl von Threads in einem Thread-Pool. Zu Beginn wird eine Aufgabe einem Thread zugewiesen und ausgeführt. Jedes Mal, wenn eine Task blockiert (I/O, ...), benachrichtigt die Task den Controller-Thread für diesen CPU-Kern und aktiviert ihn. Der Controller startet einen neuen Thread oder weckt einen zuvor schlafenden Task. Dies wird fortgesetzt, bis alle Aufgaben verarbeitet wurden. – jameszhao00

+0

Sie sind ein bisschen weg, aber ich denke, Sie haben das Wesentliche davon. Sie sollten die ThreadPool-Dokumentation (oder Google für einige Tutorials mit QueueUserWorkItem) lesen. Es werden nicht wirklich Threads für jeden Kern erstellt. Stellen Sie sich ThreadPool als eine von Kernen unabhängige Abstraktion vor. Sie werfen einfach mehrere Aufgaben darauf, die Sie wann immer möglich geplant und ausgeführt werden müssen (was normalerweise der Fall ist). – jscharf

2

Sie sollten auf jeden Fall die Concurrency and Coordination Runtime überprüfen. Eine ihrer Stichproben beschreibt genau, worüber Sie sprechen: Sie rufen zu Diensten mit langer Latenzzeit auf, und die CCR-Funktion ermöglicht effizient eine andere Aufgabe, während Sie warten. Es kann eine große Anzahl von Aufgaben bewältigen, da es nicht für jeden einen Thread erstellen muss, obwohl es alle Ihre Kerne verwendet, wenn Sie darum bitten.

0

In der Tat, wenn Sie einen Thread für eine Aufgabe verwenden, werden Sie das Spiel verlieren. Denken Sie darüber nach, warum Node.js eine große Anzahl von Verbindungen unterstützen kann. Verwenden Sie ein paar Thread mit async IO !!! Async- und Avatar-Funktionen können dabei helfen.

foreach (var task in tasks) 
{ 
    await SendAsync(task.value); 
    ReadAsync(); 
} 

SendAsync() und ReadAsync() Funktionen gefälscht IO Aufruf ASYNC.

Task parallelism ist auch eine gute Wahl. Aber ich bin mir nicht sicher, welcher schneller ist. Sie können beide in Ihrem Fall testen.

0

Ja natürlich können Sie. Sie müssen nur einen Dispatcher-Mechanismus erstellen, der auf ein von Ihnen bereitgestelltes Lambda zurückgreift und in eine Warteschlange geht. Der ganze Code, den ich in Unity schreibe, benutzt diesen Ansatz und ich verwende niemals Coroutinen. Ich verpacke Methoden, die Koroutinen wie WWW-Zeug verwenden, um es einfach loszuwerden. In der Theorie können Koroutinen schneller sein, da weniger Overhead vorhanden ist. Praktisch führen sie eine neue Syntax in eine Sprache ein, um eine ziemlich triviale Aufgabe zu erledigen, und außerdem können Sie die Stack-Verfolgung bei einem Fehler in einer Co-Routine nicht richtig verfolgen, weil alles, was Sie sehen werden, ist -> Weiter. Sie müssen dann die Fähigkeit implementieren, die Aufgaben in der Warteschlange in einem anderen Thread auszuführen. Allerdings gibt es im neuesten .net parallele Funktionen, und Sie würden im Wesentlichen ähnliche Funktionen schreiben. Es wären nicht wirklich viele Codezeilen.

Wenn jemand interessiert ist, würde ich den Code senden, habe es nicht auf mich.