Unexpected behavior for ThreadPool.QueueUserWorkItem

Please check the sample code below:

public class Sample { public int counter { get; set; } public string ID; public void RunCount() { for (int i = 0; i < counter; i++) { Thread.Sleep(1000); Console.WriteLine(this.ID + " : " + i.ToString()); } } } class Test { static void Main() { Sample[] arrSample = new Sample[4]; for (int i = 0; i < arrSample.Length; i++) { arrSample[i] = new Sample(); arrSample[i].ID = "Sample-" + i.ToString(); arrSample[i].counter = 10; } foreach (Sample s in arrSample) { ThreadPool.QueueUserWorkItem(callback => s.RunCount()); } Console.ReadKey(); } } 

The expected result for this sample should look something like this:

 Sample-0 : 0 Sample-1 : 0 Sample-2 : 0 Sample-3 : 0 Sample-0 : 1 Sample-1 : 1 Sample-2 : 1 Sample-3 : 1 . . . 

However, when you run this code, instead it will look something like this:

 Sample-3 : 0 Sample-3 : 0 Sample-3 : 0 Sample-3 : 1 Sample-3 : 1 Sample-3 : 0 Sample-3 : 2 Sample-3 : 2 Sample-3 : 1 Sample-3 : 1 . . . 

I can understand that the order in which threads are executed may differ and therefore the counter does not increase with rounding. However, I don’t understand why all ID displayed as Sample-3 , while the execution explicitly occurs independently of each other.

Arent different objects used with different threads?

+6
multithreading c # asynchronous threadpool
source share
1 answer

This is an old problem with a modified closure. You can see: Threadpools is a possible thread execution problem for a similar issue, and Eric Lippert's blog post Closing a loop variable is considered dangerous for understanding the problem.

Essentially, the lambda expression that you have captures the variable s , not the value of the variable at the point declared by lambda. Therefore, subsequent changes to the value of the variable are visible to the delegate. The Sample instance on which the RunCount method will be executed will depend on the instance referenced by the variable s (its value) at the point that the delegate actually executes.

In addition, since the delegate (the compiler actually reuses the same instance of the delegate) runs asynchronously, it is not guaranteed that these values ​​will be at the point of each execution. What you see at the moment is that the foreach ends in the main thread before any delegate call (which can be expected - it takes time to schedule tasks in the thread pool). Thus, all work items complete the "final" value of the loop variable. But this is not guaranteed by any means; try inserting a reasonable Thread.Sleep duration into the loop and you will see a different output.


Common fix:

  • Insert another variable inside the loop body.
  • Assign this variable to the current value of the loop variable.
  • Grab the 'copy' variable instead of the loop variable inside the lambda.

     foreach (Sample s in arrSample) { Sample sCopy = s; ThreadPool.QueueUserWorkItem(callback => sCopy.RunCount()); } 

Now each work item "owns" a particular value of the loop variable.


Another option in this case is to completely avoid the problem without capturing anything:

 ThreadPool.QueueUserWorkItem(obj => ((Sample)obj).RunCount(), s); 
+10
source share

All Articles