Begrenzt Parallel.ForEach die Anzahl der aktiven Threads?

107

Angesichts dieses Codes:

var arrayStrings = new string[1000];
Parallel.ForEach<string>(arrayStrings, someString =>
{
    DoSomething(someString);
});

Werden alle 1000 Threads fast gleichzeitig erscheinen?

Jader Dias
quelle

Antworten:

149

Nein, es werden nicht 1000 Threads gestartet - ja, es wird die Anzahl der verwendeten Threads begrenzt. Parallele Erweiterungen verwenden eine angemessene Anzahl von Kernen, je nachdem, wie viele Sie physisch haben und wie viele bereits beschäftigt sind. Es weist jedem Kern Arbeit zu und verwendet dann eine Technik namens Arbeitsdiebstahl, damit jeder Thread seine eigene Warteschlange effizient verarbeiten kann und nur dann teuren Cross-Thread-Zugriff ausführen muss, wenn dies wirklich erforderlich ist.

Werfen Sie einen Blick auf die PFX Team Blog für Lasten von Informationen darüber , wie es funktioniert und alle Arten von anderen Themen zuordnet.

Beachten Sie, dass Sie in einigen Fällen auch den gewünschten Parallelitätsgrad angeben können.

Jon Skeet
quelle
2
Ich habe Parallel.ForEach (FilePathArray, path => ... verwendet, um heute Abend ungefähr 24.000 Dateien zu lesen und eine neue Datei für jede eingelesene Datei zu erstellen. Sehr einfacher Code. Es scheint, dass sogar 6 Threads ausreichten, um die Festplatte mit 7200 U / min zu überfordern Ich habe mit einer Auslastung von 100% gelesen. Innerhalb weniger Stunden habe ich beobachtet, wie sich die Parallel-Bibliothek über 8.000 Threads abspaltete. Ich habe mit MaxDegreeOfParallelism getestet und sicher sind die über 8000 Threads verschwunden. Ich habe sie jetzt mehrmals mit derselben getestet Ergebnis.
Jake Drew
Es könnte 1000 Threads für etwas entartetes 'DoSomething' starten. (Wie in dem Fall, in dem ich mich derzeit mit einem Problem im Produktionscode befasse, bei dem kein Limit festgelegt wurde und mehr als 200 Threads erzeugt wurden, wodurch der SQL-Verbindungspool geöffnet wurde. Ich empfehle, den maximalen DOP für alle Arbeiten festzulegen, die nicht trivial begründet werden können ungefähr als explizit CPU-gebunden.)
user2864740
Partitionierer - docs.microsoft.com/en-us/dotnet/api/…
rafidheen
28

Auf einem Single-Core-Computer ... Parallel.ForEach-Partitionen (Chunks) der Sammlung, an denen zwischen einer Reihe von Threads gearbeitet wird. Diese Anzahl wird jedoch auf der Grundlage eines Algorithmus berechnet, der die von der Threads, die dem ForEach zugewiesen werden. Also , wenn der Körperteil des ForEach ruft zu lange laufen IO-bound / Sperrfunktionen , die den Faden herum warten lassen würden, wird der Algorithmus mehr Threads erzeugen und neu partitionieren die Sammlung zwischen ihnen . Wenn die Threads schnell abgeschlossen sind und beispielsweise keine E / A-Threads blockieren, z. B. einfach einige Zahlen berechnen,Der Algorithmus erhöht (oder verringert) die Anzahl der Threads bis zu einem Punkt, an dem der Algorithmus das Optimum für den Durchsatz berücksichtigt (durchschnittliche Abschlusszeit jeder Iteration) .

Grundsätzlich ermittelt der Thread-Pool hinter all den verschiedenen Funktionen der parallelen Bibliothek eine optimale Anzahl von zu verwendenden Threads. Die Anzahl der physischen Prozessorkerne bildet nur einen Teil der Gleichung. Es gibt KEINE einfache Eins-zu-Eins-Beziehung zwischen der Anzahl der Kerne und der Anzahl der erzeugten Threads.

Ich finde die Dokumentation zum Abbrechen und Behandeln von Synchronisierungsthreads nicht sehr hilfreich. Hoffentlich kann MS bessere Beispiele in MSDN liefern.

Vergessen Sie nicht, dass der Body-Code so geschrieben sein muss, dass er auf mehreren Threads ausgeführt werden kann, zusammen mit allen üblichen Überlegungen zur Thread-Sicherheit. Das Framework abstrahiert diesen Faktor noch nicht.

Microsoft Developer
quelle
1
"..wenn der Hauptteil von ForEach lang laufende Blockierungsfunktionen aufruft, die den Thread warten lassen, erzeugt der Algorithmus mehr Threads .." - In entarteten Fällen bedeutet dies, dass möglicherweise so viele Threads erstellt werden, wie zulässig pro ThreadPool.
user2864740
2
Sie haben Recht, für IO kann es +100 Threads zuweisen, während ich mich selbst
debuggte
5

Basierend auf der Anzahl der Prozessoren / Kerne wird eine optimale Anzahl von Threads ermittelt. Sie werden nicht alle auf einmal erscheinen.

Colin Mackay
quelle
4

Gute Frage. In Ihrem Beispiel ist der Parallelisierungsgrad selbst auf einem Quad-Core-Prozessor ziemlich niedrig, aber mit einigen Wartezeiten kann der Parallelisierungsgrad ziemlich hoch werden.

// Max concurrency: 5
[Test]
public void Memory_Operations()
{
    ConcurrentBag<int> monitor = new ConcurrentBag<int>();
    ConcurrentBag<int> monitorOut = new ConcurrentBag<int>();
    var arrayStrings = new string[1000];
    Parallel.ForEach<string>(arrayStrings, someString =>
    {
        monitor.Add(monitor.Count);
        monitor.TryTake(out int result);
        monitorOut.Add(result);
    });

    Console.WriteLine("Max concurrency: " + monitorOut.OrderByDescending(x => x).First());
}

Schauen Sie sich nun an, was passiert, wenn ein Wartevorgang hinzugefügt wird, um eine HTTP-Anforderung zu simulieren.

// Max concurrency: 34
[Test]
public void Waiting_Operations()
{
    ConcurrentBag<int> monitor = new ConcurrentBag<int>();
    ConcurrentBag<int> monitorOut = new ConcurrentBag<int>();
    var arrayStrings = new string[1000];
    Parallel.ForEach<string>(arrayStrings, someString =>
    {
        monitor.Add(monitor.Count);

        System.Threading.Thread.Sleep(1000);

        monitor.TryTake(out int result);
        monitorOut.Add(result);
    });

    Console.WriteLine("Max concurrency: " + monitorOut.OrderByDescending(x => x).First());
}

Ich habe noch keine Änderungen vorgenommen und der Grad der Parallelität / Parallelisierung ist dramatisch gestiegen. Das Limit der Parallelität kann mit erhöht werden ParallelOptions.MaxDegreeOfParallelism.

// Max concurrency: 43
[Test]
public void Test()
{
    ConcurrentBag<int> monitor = new ConcurrentBag<int>();
    ConcurrentBag<int> monitorOut = new ConcurrentBag<int>();
    var arrayStrings = new string[1000];
    var options = new ParallelOptions {MaxDegreeOfParallelism = int.MaxValue};
    Parallel.ForEach<string>(arrayStrings, options, someString =>
    {
        monitor.Add(monitor.Count);

        System.Threading.Thread.Sleep(1000);

        monitor.TryTake(out int result);
        monitorOut.Add(result);
    });

    Console.WriteLine("Max concurrency: " + monitorOut.OrderByDescending(x => x).First());
}

// Max concurrency: 391
[Test]
public void Test()
{
    ConcurrentBag<int> monitor = new ConcurrentBag<int>();
    ConcurrentBag<int> monitorOut = new ConcurrentBag<int>();
    var arrayStrings = new string[1000];
    var options = new ParallelOptions {MaxDegreeOfParallelism = int.MaxValue};
    Parallel.ForEach<string>(arrayStrings, options, someString =>
    {
        monitor.Add(monitor.Count);

        System.Threading.Thread.Sleep(100000);

        monitor.TryTake(out int result);
        monitorOut.Add(result);
    });

    Console.WriteLine("Max concurrency: " + monitorOut.OrderByDescending(x => x).First());
}

Ich empfehle die Einstellung ParallelOptions.MaxDegreeOfParallelism. Dies erhöht nicht unbedingt die Anzahl der verwendeten Threads, stellt jedoch sicher, dass Sie nur eine vernünftige Anzahl von Threads starten, was Ihr Anliegen zu sein scheint.

Um Ihre Frage zu beantworten: Nein, Sie werden nicht alle Threads gleichzeitig starten. Verwenden Sie Parallel.Invoke, wenn Sie perfekt parallel aufrufen möchten, z. B. um die Rennbedingungen zu testen.

// 636462943623363344
// 636462943623363344
// 636462943623363344
// 636462943623363344
// 636462943623363344
// 636462943623368346
// 636462943623368346
// 636462943623373351
// 636462943623393364
// 636462943623393364
[Test]
public void Test()
{
    ConcurrentBag<string> monitor = new ConcurrentBag<string>();
    ConcurrentBag<string> monitorOut = new ConcurrentBag<string>();
    var arrayStrings = new string[1000];
    var options = new ParallelOptions {MaxDegreeOfParallelism = int.MaxValue};
    Parallel.ForEach<string>(arrayStrings, options, someString =>
    {
        monitor.Add(DateTime.UtcNow.Ticks.ToString());
        monitor.TryTake(out string result);
        monitorOut.Add(result);
    });

    var startTimes = monitorOut.OrderBy(x => x.ToString()).ToList();
    Console.WriteLine(string.Join(Environment.NewLine, startTimes.Take(10)));
}
Timothy Gonzalez
quelle