web-dev-qa-db-fra.com

Les tâches de démarrage dans chaque boucle utilisent la valeur du dernier élément

Je fais une première tentative pour jouer avec les nouvelles tâches, mais il se passe quelque chose que je ne comprends pas.

Tout d'abord, le code, qui est assez simple. Je passe une liste de chemins d'accès à certains fichiers image et tente d'ajouter une tâche pour traiter chacun d'eux:

public Boolean AddPictures(IList<string> paths)
{
    Boolean result = (paths.Count > 0);
    List<Task> tasks = new List<Task>(paths.Count);

    foreach (string path in paths)
    {
        var task = Task.Factory.StartNew(() =>
            {
                Boolean taskResult = ProcessPicture(path);
                return taskResult;
            });
        task.ContinueWith(t => result &= t.Result);
        tasks.Add(task);
    }

    Task.WaitAll(tasks.ToArray());

    return result;
}

J'ai constaté que si je laisse simplement cela s'exécuter avec, disons, une liste de 3 chemins dans un test unitaire, les trois tâches utilisent le dernier chemin de la liste fournie. Si j'exécute (et ralentis le traitement de la boucle), chaque chemin de la boucle est utilisé.

Quelqu'un peut-il expliquer ce qui se passe et pourquoi? Solutions de contournement possibles?

47
Wonko the Sane

Vous fermez la variable de boucle. Ne fais pas ça. Prenez plutôt une copie:

foreach (string path in paths)
{
    string pathCopy = path;
    var task = Task.Factory.StartNew(() =>
        {
            Boolean taskResult = ProcessPicture(pathCopy);
            return taskResult;
        });
    // See note at end of post
    task.ContinueWith(t => result &= t.Result);
    tasks.Add(task);
}

Votre code actuel capture path - pas la valeur lorsque vous créez la tâche, mais la variable elle-même. Cette variable change de valeur à chaque fois que vous parcourez la boucle - elle peut donc facilement changer au moment de l'appel de votre délégué.

En prenant une copie de la variable, vous introduisez une nouvelle variable à chaque fois que vous parcourez la boucle - lorsque vous capturez cette variable , elle ne sera pas modifiée lors de la prochaine itération de la boucle.

Eric Lippert a une paire de billets de blog qui abordent ce sujet de manière beaucoup plus détaillée: partie 1 ; partie 2 .

Ne vous sentez pas mal - cela surprend presque tout le monde :(


Remarque sur cette ligne:

task.ContinueWith(t => result &= t.Result);

Comme indiqué dans les commentaires, ce n'est pas thread-safe. Plusieurs threads pourraient l'exécuter en même temps, ce qui pourrait potentiellement marquer les résultats des autres. Je n'ai pas ajouté de verrouillage ou quelque chose de similaire car cela détournerait l'attention du problème principal auquel la question est intéressée, à savoir la capture de variables. Cependant, il vaut la peine d'en être conscient.

83
Jon Skeet

Le lambda que vous passez à StartNew fait référence à la variable path, qui change à chaque itération (c'est-à-dire que votre lambda utilise référence de path, plutôt que juste sa valeur). Vous pouvez en créer une copie locale afin de ne pas pointer vers une version qui changera:

foreach (string path in paths)
{
    var lambdaPath = path;
    var task = Task.Factory.StartNew(() =>
        {
            Boolean taskResult = ProcessPicture(lambdaPath);
            return taskResult;
        });
    task.ContinueWith(t => result &= t.Result);
    tasks.Add(task);
}
12
bdukes