2 votes

Bibliothèque de tâches parallèles et boucles

J'ai rencontré une situation où les tâches que je crée ne semblent fonctionner que lorsque je débogue le code.

Comme vous le verrez ci-dessous, je continue à obtenir une exception d'index hors plage, ce qui ne devrait pas être possible puisque la boucle ne devrait jamais atteindre 5.

Si j'ajoute un point d'arrêt à la boucle et que je continue à appuyer sur F10 jusqu'à la fin du programme, tout fonctionne comme prévu.

Une idée de ce que je fais mal ?

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
class Program
{        
    static void Main(string[] args)
    {
        int[] numbers = new int[5] { 1, 2, 3, 4, 5 };
        List<int> nums = new List<int>();

        var tasks = new Task[5];

        for (int i = 0; i < numbers.Length; i++)
        {
            tasks[i] = Task.Factory.StartNew(() =>
                {
                    nums.Add(numbers[i]);
                }, 
                TaskCreationOptions.None);
        }
        Task.WaitAll(tasks);

        for (int i = 0; i < nums.Count; i++)
        {
            Console.WriteLine(nums[i]);
        }
        Console.ReadLine();
    }
}

}

Je m'attendrais à voir 1, 2, 3, 4, 5, mais pas dans un ordre particulier, car l'exécution est asynchrone.

Bizarrement, cela fonctionne, mais je ne vois pas la différence, si ce n'est la saisie supplémentaire.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
class Program
{        
    static void Main(string[] args)
    {
        int[] numbers = new int[5] { 1, 2, 3, 4, 5 };
        List<int> nums = new List<int>();

        var tasks = new Task[5]
        {
            Task.Factory.StartNew(() => {nums.Add(numbers[0]);}, TaskCreationOptions.None),
            Task.Factory.StartNew(() => {nums.Add(numbers[1]);}, TaskCreationOptions.None),
            Task.Factory.StartNew(() => {nums.Add(numbers[2]);}, TaskCreationOptions.None),
            Task.Factory.StartNew(() => {nums.Add(numbers[3]);}, TaskCreationOptions.None),
            Task.Factory.StartNew(() => {nums.Add(numbers[4]);}, TaskCreationOptions.None)
        };

        Task.WaitAll(tasks);

        for (int i = 0; i < nums.Count; i++)
        {
            Console.WriteLine(nums[i]);
        }
        Console.ReadLine();
    }
}

}

Remerciements

Mike

3voto

Tom Points 1790

Comme la tâche se déroule en dehors de la boucle, qui s'est terminée au moment où la tâche s'exécute, i est à ce moment-là égal à 5 (condition de sortie de la boucle).

Dans votre deuxième exemple, vous n'utilisez pas i, mais vous codifiez les valeurs en dur, de sorte que le problème disparaît.

Dans le débogueur, la synchronisation est différente et les tâches peuvent s'exécuter avant que la boucle ne se termine : là encore, le problème disparaît.

Je pense que l'on peut résoudre le problème de la manière suivante :

var ii=i;
tasks[i] = Task.Factory.StartNew(() =>
                {
                    nums.Add(numbers[ii]);
                }, 
                TaskCreationOptions.None);

3voto

Ronald Wildenberg Points 18258

MISE À JOUR : En C#5, ce problème n'existe plus. A rupture changer était afin que la variable de boucle d'un foreach soit logiquement à l'intérieur de la boucle.

Voici un nouvel exemple de l'une des erreurs les plus courantes que l'on peut commettre dans le code C# : capturer la variable de la boucle dans un délégué anonyme. Eric Lippert propose un bonne explication de ce qui ne va pas.

En fait, votre situation est encore pire car, en théorie, tout peut aller bien ou mal de manière aléatoire. Supposons que vous ayez effectué la modification suivante :

for (int i = 0; i < numbers.Length; i++)
{
    tasks[i] = ...;
    tasks[i].Wait();
}

votre programme fonctionne soudain comme prévu. La raison en est que votre tâche prend la valeur de i (la variable de la boucle) lorsque la tâche s'exécute et non la valeur de i lorsque cette tâche a été créée. La séquence suivante est donc possible dans votre programme original :

i = 0
Create task 0
i = 1
Run task 0: This task sees i = 1
Create task 1
i = 2
Create task 2
i = 3
Create task 3
i = 4
Create task 4
i = 5
Run task 1: This task sees i = 5 and throws an exception

La réponse de Tom résout ce problème en introduisant une nouvelle variable ii à l'intérieur de la boucle. Lorsque chaque tâche est créée, elle capture cette variable, qui a une valeur fixe pour chaque itération.

2voto

Emad Omara Points 472

Le problème ici est que la tâche accède à la variable i, et au moment où la tâche s'exécute, cette variable sera modifiée, et la raison pour laquelle vous obtenez l'exception dans la dernière itération, la variable i sera 5, et bien que la boucle s'arrête après cette incrémentation, mais le corps de la tâche continue à la référencer, donc cette ligne lancera

nums.Add(numbers[i]);

car il est évident que 5 est une valeur hors norme.

Pour résoudre ce problème, vous devez passer i à la méthode StartNew en tant que paramètre d'état.

 tasks[i] = Task.Factory.StartNew((obj) =>
            {
                int index = (int) obj;
                nums.Add(numbers[index]);
            }, 
            i);

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X