Parallel.ForEach limite-t-il le nombre de threads actifs?

107

Compte tenu de ce code:

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

Les 1000 threads vont-ils apparaître presque simultanément?

Jader Dias
la source

Réponses:

149

Non, cela ne démarrera pas 1000 threads - oui, cela limitera le nombre de threads utilisés. Les extensions parallèles utilisent un nombre approprié de cœurs, en fonction du nombre que vous en avez physiquement et du nombre déjà occupé. Il alloue le travail pour chaque noyau et utilise ensuite une technique appelée vol de travail pour permettre à chaque thread de traiter efficacement sa propre file d'attente et n'a besoin de faire un accès inter-thread coûteux que lorsqu'il en a vraiment besoin.

Jetez un oeil à l' équipe PFX Blog pour des charges d'informations sur la façon dont il répartit le travail et toutes sortes d'autres sujets.

Notez que dans certains cas, vous pouvez également spécifier le degré de parallélisme souhaité.

Jon Skeet
la source
2
J'utilisais Parallel.ForEach (FilePathArray, path => ... pour lire environ 24 000 fichiers ce soir en créant un nouveau fichier pour chaque fichier que j'ai lu. Code très simple. Il semble que même 6 threads suffisaient à submerger le disque 7200 RPM Je lisais à 100% d'utilisation. Au cours de la période de quelques heures, j'ai regardé la bibliothèque Parallel tourner plus de 8 000 threads. J'ai testé avec MaxDegreeOfParallelism et bien sûr, plus de 8000 threads ont disparu. Je l'ai testé plusieurs fois maintenant avec le même résultat.
Jake Drew
Il pourrait démarrer 1000 threads pour certains «DoSomething» dégénérés. (Comme dans le cas où je suis actuellement confronté à un problème dans le code de production qui n'a pas réussi à définir une limite et a engendré plus de 200 threads, faisant ainsi apparaître le pool de connexions SQL. Je recommande de définir le Max DOP pour tout travail qui ne peut pas être raisonnablement raisonné environ comme étant explicitement lié au processeur.)
user2864740
Partitionneur - docs.microsoft.com/en-us/dotnet/api/…
rafidheen
28

Sur une machine à un seul cœur ... Parallèle.Pour chaque partition (morceaux) de la collection sur laquelle elle travaille entre un certain nombre de threads, mais ce nombre est calculé en fonction d'un algorithme qui prend en compte et semble surveiller en permanence le travail effectué par le threads qu'il alloue au ForEach. Ainsi, si la partie corps du ForEach appelle des fonctions liées / bloquantes d'E / S de longue durée qui laisseraient le thread en attente, l'algorithme engendrera plus de threads et repartitionnera la collection entre eux . Si les threads se terminent rapidement et ne se bloquent pas sur les threads IO par exemple, comme le simple calcul de certains nombres,l'algorithme augmentera (voire diminuera) le nombre de threads jusqu'à un point où l'algorithme considère comme optimal pour le débit (temps de réalisation moyen de chaque itération) .

Fondamentalement, le pool de threads derrière toutes les différentes fonctions de la bibliothèque Parallel déterminera un nombre optimal de threads à utiliser. Le nombre de cœurs de processeur physique ne constitue qu'une partie de l'équation. Il n'y a PAS une simple relation un à un entre le nombre de cœurs et le nombre de threads générés.

Je ne trouve pas la documentation sur l'annulation et la gestion des threads de synchronisation très utile. Espérons que MS peut fournir de meilleurs exemples dans MSDN.

N'oubliez pas que le code du corps doit être écrit pour s'exécuter sur plusieurs threads, avec toutes les considérations habituelles de sécurité des threads, le framework n'abstrait pas ce facteur ... pour le moment.

Développeur Microsoft
la source
1
"..si la partie corps du ForEach appelle des fonctions de blocage de longue durée qui laisseraient le thread en attente, l'algorithme engendrera plus de threads .." - Dans des cas dégénérés, cela signifie qu'il peut y avoir autant de threads créés que permis par ThreadPool.
user2864740
2
Vous avez raison, pour IO, il peut allouer +100 threads lorsque je me suis débogué
FindOutIslamNow
5

Il établit un nombre optimal de threads en fonction du nombre de processeurs / cœurs. Ils n'apparaîtront pas tous en même temps.

Colin Mackay
la source
5

Voir Est - ce que Parallel.For utilise une tâche par itération? pour une idée d'un «modèle mental» à utiliser. Cependant, l'auteur déclare que "En fin de compte, il est important de se rappeler que les détails de mise en œuvre peuvent changer à tout moment."

Kevin Hakanson
la source
4

Excellente question. Dans votre exemple, le niveau de parallélisation est assez bas même sur un processeur quad core, mais avec une certaine attente, le niveau de parallélisation peut devenir assez élevé.

// 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());
}

Regardez maintenant ce qui se passe lorsqu'une opération en attente est ajoutée pour simuler une requête HTTP.

// 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());
}

Je n'ai pas encore apporté de modifications et le niveau de concurrence / parallélisation a considérablement augmenté. La concurrence peut avoir sa limite augmentée avec 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());
}

Je recommande le réglage ParallelOptions.MaxDegreeOfParallelism. Cela n'augmentera pas nécessairement le nombre de threads en cours d'utilisation, mais cela vous assurera de ne démarrer qu'un nombre raisonnable de threads, ce qui semble être votre préoccupation.

Enfin, pour répondre à votre question, non, vous n'obtiendrez pas tous les fils de discussion à la fois. Utilisez Parallel.Invoke si vous cherchez à invoquer parfaitement en parallèle, par exemple pour tester les conditions de course.

// 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
la source