Zwei Threads ein Kern

8

Ich spiele mit einer einfachen Konsolen-App herum, die einen Thread erstellt, und ich führe eine Inter-Thread-Kommunikation zwischen dem Haupt- und dem Worker-Thread durch.

Ich poste Objekte aus dem Hauptthread in eine gleichzeitige Warteschlange und der Worker-Thread entnimmt diese und führt eine Verarbeitung durch.

Was mir seltsam vorkommt, ist, dass ich diese App profiliere, obwohl ich zwei Kerne habe. Ein Kern ist 100% frei und der andere Kern hat die ganze Arbeit erledigt, und ich sehe, dass beide Threads in diesem Kern laufen.

Warum ist das?

Liegt es daran, dass ich ein Wait-Handle verwende, das beim Senden einer Nachricht gesetzt wird und nach Abschluss der Verarbeitung wieder freigibt?

Dies ist mein Beispielcode, der jetzt zwei Arbeitsthreads verwendet. Es verhält sich immer noch gleich, main, worker1 und worker2 laufen im selben Kern. Ideen?

[BEARBEITEN] Es funktioniert jetzt zumindest, ich bekomme doppelt so viel Leistung wie gestern. Der Trick bestand darin, den Verbraucher gerade genug zu verlangsamen, um die Signalisierung mit dem AutoResetEvent zu vermeiden.

%Vor%     
Roger Johansson 13.01.2014, 15:32
quelle

4 Antworten

7
  

Liegt es daran, dass ich ein Wait-Handle verwende, das beim Senden einer Nachricht gesetzt wird und nach Abschluss der Verarbeitung wieder freigibt?

Ohne Ihren Code zu sehen, ist es schwer zu sagen, aber aus Ihrer Beschreibung scheint es, dass die zwei Threads, die Sie geschrieben haben, als Co-Routinen fungieren: Wenn der Haupt-Thread läuft, hat der Worker-Thread nichts zu tun und umgekehrt. Es sieht so aus, als wäre der .NET-Scheduler schlau genug, um den zweiten Kern nicht zu laden, wenn dies passiert.

Sie können dieses Verhalten auf verschiedene Arten ändern - zum Beispiel

  • indem Sie etwas am Hauptthread arbeiten, bevor Sie auf den Handle warten, oder
  • , indem Sie weitere Worker-Threads hinzufügen, die um die Aufgaben konkurrieren, die Ihr Hauptthread veröffentlicht, und beide könnten eine Aufgabe zum Bearbeiten erhalten.
dasblinkenlight 13.01.2014, 15:40
quelle
2

OK, ich habe herausgefunden, was das Problem ist. Der Produzent und Konsument ist in diesem Fall ziemlich genau so schnell. Dies führt dazu, dass der Verbraucher seine gesamte Arbeit schnell beendet und dann zurückläuft, um auf das AutoResetEvent zu warten. Wenn der Produzent das nächste Mal eine Aufgabe sendet, muss er das AutoresetEvent berühren und einstellen.

Die Lösung bestand darin, dem Verbraucher eine sehr kleine Verzögerung hinzuzufügen, wodurch er etwas langsamer als der Produzent wurde. Dies führt dazu, dass der Produzent, wenn er eine Aufgabe sendet, merkt, dass der Konsument bereits aktiv ist und nur in die Worker-Warteschlange schreiben muss, ohne das AutoResetEvent zu berühren.

Das ursprüngliche Verhalten führte zu einer Art Ping-Pong-Effekt, der auf dem Screenshot zu sehen ist.

    
Roger Johansson 14.01.2014 10:35
quelle
1

Dasblinkelight (wahrscheinlich) hat die richtige Antwort.

Abgesehen davon wäre es auch das korrekte Verhalten, wenn einer Ihrer Threads I / O-gebunden ist (das heißt, er hängt nicht auf der CPU) - in diesem Fall haben Sie von der Verwendung mehrerer Kerne nichts zu gewinnen, und .NET ist schlau genug, um nur Kontexte auf einem Kern zu ändern.

Dies ist häufig bei UI-Threads der Fall - es hat nur sehr wenig zu tun, so dass es normalerweise keinen Grund gibt, einen ganzen Kern für sich zu beanspruchen. Und ja, wenn Ihre gleichzeitige Warteschlange nicht richtig verwendet wird, könnte es einfach bedeuten, dass der Haupt-Thread auf den Worker-Thread wartet - auch in diesem Fall müssen die Kerne nicht gewechselt werden, da der ursprüngliche Thread sowieso wartet.

    
Luaan 13.01.2014 15:45
quelle
1

Sie sollten BlockingCollection anstelle von ConcurrentQueue verwenden. . Standardmäßig verwendet BlockingCollection ein ConcurrentQueue unter der Haube, aber es hat eine viel einfacher zu bedienende Oberfläche. Insbesondere wartet es nicht besetzt. Darüber hinaus unterstützt BlockingCollection Stornierung , also Ihr Verbraucher wird sehr einfach. Hier ist ein Beispiel:

%Vor%

Die Schleife mit GetConsumingEnumerable führt eine Wartezeit in der Warteschlange aus. Es ist nicht nötig, dies mit einer separaten Veranstaltung zu tun. Es wird auf das Hinzufügen eines Elements zur Warteschlange warten oder es wird beendet, wenn Sie das Löschungs-Token setzen.

Um es normal zu stoppen, rufen Sie einfach _queue.CompleteAdding() auf. Das sagt dem Verbraucher, dass der Warteschlange keine weiteren Einträge hinzugefügt werden. Der Benutzer wird die Warteschlange leeren und dann beenden.

Wenn du früh aufhören willst, rufe einfach _cancellation.Cancel() an. Das bewirkt, dass GetConsumingEnumerable beendet wird.

Im Allgemeinen sollten Sie ConcurrentQueue nicht direkt verwenden. BlockingCollection ist einfacher zu verwenden und bietet eine gleichwertige Leistung.

    
Jim Mischel 14.01.2014 17:33
quelle