Parameters in Asynchronous Lambdas

I try to run several tasks at the same time, and I ran into a problem that I cannot understand and cannot solve.

I had a function like this:

private void async DoThings(int index, bool b) { await SomeAsynchronousTasks(); var item = items[index]; item.DoSomeProcessing(); if(b) AVolatileList[index] = item; //volatile or not, it does not work else AnotherVolatileList[index] = item; } 

What I wanted to call in a for loop using Task.Run() . However, I could not find a way to send parameters to this Action<int, bool> , and everyone recommends using lambdas in such cases:

 for(int index = 0; index < MAX; index++) { //let say that MAX equals 400 bool b = CheckSomething(); Task.Run(async () => { await SomeAsynchronousTasks(); var item = items[index]; //here, index is always evaluated at 400 item.DoSomeProcessing(); if(b) AVolatileList[index] = item; //volatile or not, it does not work else AnotherVolatileList[index] = item; } } 

I thought that using local variables in lambdas would β€œcapture” their values, but it doesn't seem like that; it will always take the index value, as if the value were fixed at the end of the for loop. The index variable is evaluated at 400 in lambda at each iteration, so of course I get an IndexOutOfRangeException 400 times ( items.Count is actually MAX ).

I'm really not sure what is going on here (although it is very interesting to me), and I do not know how to do what I am trying to achieve. Any hints are welcome!

+4
source share
2 answers

Make a local copy of your index variable:

 for(int index = 0; index < MAX; index++) { var localIndex = index; Task.Run(async () => { await SomeAsynchronousTasks(); var item = items[index]; item.DoSomeProcessing(); if(b) AVolatileList[index] = item; else AnotherVolatileList[index] = item; } } 

This is because C # executes a for loop: only one index variable is updated, and all your lambdas capture the same variable (with lambdas, variables are captured, not values).

As a note, I recommend you:

  • Avoid async void . You will never know when the async void method is completed and they have complex error handling semantics.
  • await all your asynchronous operations. Ie, don't ignore the task returned from Task.Run . Use Task.WhenAll or the like for await for them. This allows you to throw exceptions.

For example, here is one way to use WhenAll :

 var tasks = Enumerable.Range(0, MAX).Select(index => Task.Run(async () => { await SomeAsynchronousTasks(); var item = items[localIndex]; item.DoSomeProcessing(); if(b) AVolatileList[localIndex] = item; else AnotherVolatileList[localIndex] = item; })); await Task.WhenAll(tasks); 
+5
source

All your lambdas capture the same variable, which is your loop variable. However, all of your lambdas are only executed after the loop finishes. At this point, the loop variable has the maximum value, so all your lambdas use it.

Stephen Cleary shows in his answer how to fix this.

Eric Lippert wrote a detailed two-part series about this.

+1
source

Source: https://habr.com/ru/post/1490716/


All Articles