Wann sollte der Thread-Pool in C # verwendet werden?

Ich habe versucht, Multithreading-Programmierung in C # zu lernen, und ich bin verwirrt, wenn es am besten ist, einen Thread-Pool zu verwenden oder meine eigenen Threads zu erstellen. Ein Buch empfiehlt, einen Thread-Pool nur für kleine Aufgaben zu verwenden (was auch immer das bedeutet), aber ich kann keine wirklichen Richtlinien finden. Was sind einige Überlegungen, die Sie bei dieser Programmierentscheidung beachten?

Wenn Sie viele logische Aufgaben haben, die eine konstante Verarbeitung erfordern und dies parallel ausgeführt werden soll, verwenden Sie den Pool + Scheduler.

Wenn Sie Ihre E / A-bezogenen Aufgaben gleichzeitig ausführen müssen, z. B. das Herunterladen von Daten von Remote-Servern oder den Festplattenzugriff, müssen Sie dies alle paar Minuten wiederholen. Erstellen Sie dann Ihre eigenen Threads und beenden Sie sie, sobald Sie fertig sind.

Bearbeiten: Über einige Überlegungen verwende ich Thread-Pools für den databasezugriff, Physik / Simulation, AI (Spiele) und für skriptbasierte Aufgaben, die auf virtuellen Maschinen ausgeführt werden, die viele benutzerdefinierte Aufgaben verarbeiten.

Normalerweise besteht ein Pool aus 2 Threads pro processor (also wahrscheinlich 4 heutzutage), aber Sie können die Anzahl der benötigten Threads einstellen, wenn Sie wissen, wie viele Sie benötigen.

Bearbeiten: Der Grund, eigene Threads zu erstellen, liegt an Kontextänderungen (das heißt, wenn Threads zusammen mit dem Speicher in den process und aus dem process ausgelagert werden müssen). Wenn Sie unnütze Kontextänderungen verwenden, sagen Sie, wenn Sie Ihre Threads nicht verwenden, lassen Sie sie einfach so sitzen, wie Sie vielleicht sagen, kann die performance Ihres Programms leicht halbieren (sagen Sie, Sie haben 3 schlafende Threads und 2 aktive Threads). Wenn also die Download-Threads nur warten, verbrauchen sie tonnenweise CPU und kühlen den Cache für Ihre echte Anwendung ab

Ich würde vorschlagen, dass Sie einen Threadpool in C # aus den gleichen Gründen wie jede andere Sprache verwenden.

Wenn Sie die Anzahl der laufenden Threads beschränken oder nicht den Aufwand für das Erstellen und Löschen von Threads verwenden möchten, verwenden Sie einen Thread-Pool.

Bei kleinen Aufgaben bedeutet das Buch, das Sie lesen, Aufgaben mit einer kurzen Lebensdauer. Wenn es zehn Sekunden dauert, um einen Thread zu erstellen, der nur für eine Sekunde läuft, ist das ein Ort, an dem Sie Pools verwenden sollten (ignorieren Sie meine tatsächlichen Zahlen, es ist das Verhältnis, das zählt).

Sonst verbringst du den Großteil deiner Zeit damit, Threads zu erstellen und zu zerstören, anstatt einfach nur die Arbeit zu verrichten, für die sie bestimmt sind.

Hier ist eine nette Zusammenfassung des Thread-Pools in .Net: http://blogs.msdn.com/pedram/archive/2007/08/05/dedicated-thread-or-threadpool-thread.aspx

Der Beitrag enthält auch einige Punkte, wenn Sie den Thread-Pool nicht verwenden und stattdessen einen eigenen Thread erstellen sollten.

Ich empfehle das Lesen dieses kostenlosen E-Books: Threading in C # von Joseph Albahari

Lesen Sie zumindest den Abschnitt “Erste Schritte”. Das E-Book bietet eine gute Einführung und enthält eine Fülle von fortgeschrittenen Threading-Informationen.

Zu wissen, ob der Thread-Pool verwendet werden soll oder nicht, ist nur der Anfang. Als nächstes müssen Sie bestimmen, welche Methode für die Eingabe des Thread-Pools Ihren Anforderungen am besten entspricht:

  • Task Parallelbibliothek (.NET Framework 4.0)
  • ThreadPool.QueueUserWorkItem
  • Asynchrone Delegaten
  • Hintergrundarbeiter

Dieses E-Book erklärt all diese und empfiehlt, sie zu verwenden oder einen eigenen Thread zu erstellen.

Der Thread-Pool soll den Kontextwechsel zwischen Ihren Threads reduzieren. Betrachten Sie einen process, bei dem mehrere Komponenten ausgeführt werden. Jede dieser Komponenten könnte Worker-Threads erstellen. Je mehr Threads in Ihrem process sind, desto mehr Zeit wird für den Kontextwechsel verschwendet.

Wenn nun jede dieser Komponenten Elemente in den Thread-Pool einreiht, würde der Aufwand für den Kontextwechsel viel geringer sein.

Der Thread-Pool wurde entwickelt, um die Arbeit auf allen CPUs (oder CPU-coreen) zu maximieren. Aus diesem Grund spult der Thread-Pool standardmäßig mehrere Threads pro processor.

Es gibt Situationen, in denen Sie den Thread-Pool nicht verwenden möchten. Wenn Sie auf E / A warten oder auf ein Ereignis warten usw., binden Sie diesen Thread-Pool-Thread und er kann nicht von anderen Personen verwendet werden. Dieselbe Idee gilt für lange laufende Aufgaben, obwohl das, was eine lang andauernde Aufgabe darstellt, subjektiv ist.

Pax Diablo macht auch einen guten Punkt. Spinning Threads ist nicht kostenlos. Es braucht Zeit und sie verbrauchen zusätzlichen Speicher für ihren Stack-Speicherplatz. Der Thread-Pool verwendet Threads erneut, um diese Kosten zu amortisieren.

Hinweis: Sie haben gefragt, ob Sie einen Thread-Pool-Thread zum Herunterladen von Daten oder zum Ausführen von Datenträger-E / A verwenden möchten. Sie sollten dafür keinen Thread verwenden (aus den oben genannten Gründen). Verwenden Sie stattdessen asynchrone E / A (die Methoden BeginXX und EndXX). Bei einem FileStream wären dies BeginRead und EndRead . Für eine HttpWebRequest wäre das BeginGetResponse und EndGetResponse . Sie sind komplizierter zu verwenden, aber sie sind der richtige Weg, Multithread-I / O durchzuführen.

Hüten Sie sich vor dem .NET-Thread-Pool für Operationen, die möglicherweise für einen signifikanten, variablen oder unbekannten Teil ihrer Verarbeitung blockieren, da sie anfällig für Threading-Hunger ist. Erwägen Sie, die parallelen .NET-Erweiterungen zu verwenden, die eine gute Anzahl von logischen Abstraktionen über Thread-Operationen bereitstellen. Sie enthalten auch einen neuen Scheduler, der eine Verbesserung von ThreadPool darstellen sollte. Siehe hier

Ein Grund, den Thread-Pool nur für kleine Aufgaben zu verwenden, ist die begrenzte Anzahl von Thread-Pool-Threads. Wenn einer für eine lange Zeit verwendet wird, stoppt es, dass der Thread von anderem Code verwendet wird. Wenn dies viele Male passiert, kann der Thread-Pool aufgebraucht werden.

Die Verwendung des Thread-Pools kann zu subtilen Effekten führen – einige .NET-Timer verwenden Thread-Pool-Threads und z. B. werden sie nicht ausgetriggers.

Für die höchste performance mit gleichzeitig ausgeführten Einheiten schreiben Sie Ihren eigenen Thread-Pool, wo ein Pool von Thread-Objekten beim Start erstellt wird und zum Blockieren (früher gesperrt) geht und auf einen Kontext wartet, um ausgeführt zu werden (ein Objekt mit einer von dein Code).

So viele Artikel über Tasks vs. Threads vs. .NET ThreadPool geben Ihnen wirklich nicht, was Sie brauchen, um eine Entscheidung für die performance zu treffen. Aber wenn man sie vergleicht, gewinnen Threads und vor allem ein Pool von Threads. Sie werden am besten über CPUs verteilt und sie starten schneller.

Was diskutiert werden sollte, ist die Tatsache, dass die Hauptausführungseinheit von Windows (einschließlich Windows 10) ein Thread ist und der OS-Kontext-Switching-Overhead normalerweise vernachlässigbar ist. Einfach gesagt, konnte ich viele dieser Artikel nicht überzeugend nachweisen, ob der Artikel eine höhere performance durch Speichern von Kontextwechsel oder eine bessere CPU-Auslastung beansprucht.

Jetzt für ein bisschen Realismus:

Die meisten von uns werden nicht brauchen, dass unsere Anwendung deterministisch ist, und die meisten von uns haben keinen harten Hintergrund mit Threads, der zum Beispiel oft mit der Entwicklung eines Betriebssystems einhergeht. Was ich oben geschrieben habe, ist nicht für einen Anfänger.

Was am wichtigsten ist, ist zu diskutieren, was einfach zu programmieren ist.

Wenn Sie Ihren eigenen Thread-Pool erstellen, müssen Sie etwas schreiben, da Sie sich mit der Verfolgung des Ausführungsstatus, dem Simulieren von Aussetzen und Fortsetzen und dem Abbrechen der Ausführung – auch in einer Anwendung – befassen müssen Herunterfahren. Möglicherweise müssen Sie sich auch Gedanken darüber machen, ob Sie Ihren Pool dynamisch erweitern möchten und welche Kapazitätsbeschränkungen Ihr Pool haben wird. Ich kann so einen Rahmen in einer Stunde schreiben, aber das ist, weil ich es so oft gemacht habe.

Der einfachste Weg, eine Ausführungseinheit zu schreiben, ist die Verwendung einer Aufgabe. Das Schöne an einer Aufgabe ist, dass Sie eine erstellen und in Ihrem Code inline starten können (auch wenn Vorsicht geboten ist). Sie können ein Löschungs-Token übergeben, das bearbeitet werden soll, wenn Sie die Aufgabe abbrechen möchten. Außerdem verwendet es den vielversprechenden Ansatz zum Verketten von Ereignissen, und Sie können einen bestimmten Werttyp zurückgeben lassen. Außerdem, mit async und warten Sie, gibt es mehr Optionen und Ihr Code wird portabler.

Im Wesentlichen ist es wichtig, das Für und Wider mit Tasks vs. Threads gegenüber dem .NET ThreadPool zu verstehen. Wenn ich eine hohe performance benötige, werde ich Threads verwenden und bevorzuge meinen eigenen Pool.

Eine einfache Möglichkeit zum Vergleichen besteht darin, 512 Threads, 512 Tasks und 512 ThreadPool-Threads zu starten. Sie werden eine Verzögerung am Anfang mit Threads finden (warum also einen Thread-Pool schreiben), aber alle 512 Threads werden in wenigen Sekunden ausgeführt, während Tasks und .NET ThreadPool-Threads einige Minuten dauern.

Im Folgenden sind die Ergebnisse eines solchen Tests (i5-Quad-Core mit 16 GB RAM), die jeweils 30 Sekunden zu laufen. Der ausgeführte Code führt einfache Datei-I / O auf einem SSD-Laufwerk aus.

Testergebnisse

Thread-Pools sind ideal, wenn Sie mehr Aufgaben als verfügbare Threads verarbeiten müssen.

Sie können einem Thread-Pool alle Aufgaben hinzufügen und die maximale Anzahl von Threads angeben, die zu einem bestimmten Zeitpunkt ausgeführt werden können.

Überprüfen Sie diese Seite auf MSDN: http://msdn.microsoft.com/en-us/library/3dasc8as(VS.80).aspx

Verwenden Sie immer einen Thread-Pool, wenn Sie können, arbeiten Sie auf dem höchstmöglichen Abstraktionsniveau. Thread-Pools verbergen das Erstellen und Zerstören von Threads für dich, das ist normalerweise eine gute Sache!

Die meiste Zeit können Sie den Pool verwenden, da Sie den teuren process des Erstellens des Threads vermeiden.

In einigen Szenarien möchten Sie jedoch möglicherweise einen Thread erstellen. Zum Beispiel, wenn Sie nicht der einzige sind, der den Thread-Pool verwendet und der Thread, den Sie erstellen, langlebig ist (um den Verbrauch von gemeinsam genutzten Ressourcen zu vermeiden) oder wenn Sie zum Beispiel die Stackgröße des Threads kontrollieren wollen.

Wenn Sie eine Hintergrundaufgabe haben, die für eine lange Zeit bestehen wird, wie für die gesamte Lebensdauer Ihrer Anwendung, dann ist die Erstellung eines eigenen Threads eine vernünftige Sache. Wenn Sie kurze Jobs haben, die in einem Thread ausgeführt werden müssen, verwenden Sie das Thread-Pooling.

In einer Anwendung, in der Sie viele Threads erstellen, wird der Aufwand zum Erstellen der Threads erheblich. Wenn Sie den Thread-Pool verwenden, werden die Threads einmal erstellt und wiederverwendet, wodurch der Thread-Erstellungsaufwand vermieden wird.

In einer Anwendung, an der ich arbeitete, half der Wechsel vom Erstellen von Threads zum Verwenden des Thread-Pools für die kurzlebigen Threads wirklich den Durchsatz der Anwendung.

Vergessen Sie nicht, den Hintergrundarbeiter zu untersuchen.

Ich finde für viele Situationen, es gibt mir genau das, was ich ohne das schwere Heben will.

Prost.

Normalerweise benutze ich den Threadpool immer dann, wenn ich etwas in einem anderen Thread machen muss und es nicht wirklich interessiert, wann es läuft oder endet. Etwas wie das Loggen oder vielleicht sogar Hintergrund-Herunterladen einer Datei (obwohl es bessere Möglichkeiten gibt, diesen Async-Stil zu machen). Ich benutze meinen eigenen Thread, wenn ich mehr Kontrolle brauche. Was ich gefunden habe, ist die Verwendung einer ThreadSafe-Warteschlange (eigene Hacks) zum Speichern von “Befehlsobjekten”, wenn ich mehrere Befehle habe, an denen ich in> 1 Thread arbeiten muss. Sie könnten also eine Xml-Datei aufteilen und jedes Element in eine Warteschlange stellen und dann mehrere Threads bearbeiten, um diese Elemente zu bearbeiten. Ich habe so eine Queue zurück in Uni geschrieben (VB.net!), Die ich in C # umgewandelt habe. Ich habe es unten ohne besonderen Grund eingeschlossen (dieser Code könnte einige Fehler enthalten).

 using System.Collections.Generic; using System.Threading; namespace ThreadSafeQueue { public class ThreadSafeQueue { private Queue _queue; public ThreadSafeQueue() { _queue = new Queue(); } public void EnqueueSafe(T item) { lock ( this ) { _queue.Enqueue(item); if ( _queue.Count >= 1 ) Monitor.Pulse(this); } } public T DequeueSafe() { lock ( this ) { while ( _queue.Count < = 0 ) Monitor.Wait(this); return this.DeEnqueueUnblock(); } } private T DeEnqueueUnblock() { return _queue.Dequeue(); } } } 

Ich wollte, dass ein Thread-Pool die Arbeit mit möglichst geringer Latenz über die coree verteilt, und das musste nicht gut mit anderen Anwendungen funktionieren. Ich fand, dass die performance des .NET-Thread-Pools nicht so gut war, wie sie sein könnte. Ich wusste, dass ich einen Thread pro core wollte, also schrieb ich meine eigene Thread-Pool-Ersatzklasse. Der Code wird hier als Antwort auf eine andere StackOverflow-Frage bereitgestellt.

Was die ursprüngliche Frage betrifft, ist der Thread-Pool nützlich, um sich wiederholende Berechnungen in Teile aufzuteilen, die parallel ausgeführt werden können (vorausgesetzt, sie können parallel ausgeführt werden, ohne das Ergebnis zu ändern). Manuelle Thread-Verwaltung ist nützlich für Aufgaben wie UI und IO.