Running tasks in foreach Loop uses the value of the last element

I am making my first attempt to play with new Tasks, but something happens that I do not understand.

Firstly, the code is pretty straight forward. I pass a list of paths to some image files and try to add a task to process each of them:

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; } 

I found that if I just let it run, say, a list of three paths in unit test, all three tasks use the last path in the specified list. If I pass (and slow down the processing of the loop), each path from the loop is used.

Can someone explain what is happening and why? Possible workarounds?

+38
multithreading c # task-parallel-library
Jan 13 '11 at 19:27
source share
2 answers

You close the loop variable. Do not do this. Instead, make a copy:

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

Your current code captures path - not the value of it when creating the task, but the variable itself. This variable changes the value every time you go through the loop, so it can easily change by the time you call the delegate.

Having accepted a copy of the variable, you introduce a new variable every time you go through the loop β€” when you commit this variable, it will not be changed in the next iteration of the loop.

Eric Lippert has a couple of blog posts that go into this in much more detail: Part 1 ; part 2 .

I don’t feel bad - it catches almost everyone :(

+75
Jan 13 '11 at 19:29
source share

The lambda passed to StartNew refers to the path variable that changes at each iteration (i.e. your lambda uses the path reference, not just its value). You can create a local copy of this file so as not to point to the version that will change:

 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
Jan 13 '11 at 19:29
source share



All Articles