Please check the code sample 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 output for this sample should be something like :
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, it would show something like this instead:
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 the threads are executing might differ and hence the count isnt increasing in round robin fashion. However, I fail to understand, why all the ID
s are being displayed as Sample-3
, while the execution is clearly happening independent of each other.
Arent different objects being used with different threads?
QueueUserWorkItem(WaitCallback, Object) Queues a method for execution, and specifies an object containing data to be used by the method. The method executes when a thread pool thread becomes available.
You can wait for a task to finish in a ThreadPoolExecutor by calling the wait() module function.
Thread pool in C# is a collection of threads. It is used to perform tasks in the background. When a thread completes a task, it is sent to the queue wherein all the waiting threads are present. This is done so that it can be reused.
This is the old modified closure problem. You might want to look at: Threadpools - possible thread execution order problem for a similar question, and Eric Lippert's blog post Closing over the loop variable considered harmful for an understanding of the issue.
Essentially, the lambda expression you've got there is capturing the variable s
rather than the value of the variable at the point the lambda is declared. Consequently, subsequent changes made to the value of the variable are visible to the delegate. The instance of Sample
on which the RunCount
method will run will depend on the instance referred to by the variable s
(its value) at the point the delegate actually executes.
Additionally, since the delegate(s) (the compiler actually reuses the same delegate instance) are being asynchronously executed, it isn't guaranteed what these values will be at the point of each execution. What you are currently seeing is that the foreach
loop completes on the main-thread before any of the delegate-invocations (to be expected - it takes time to schedule tasks on the thread-pool). So all the work-items end up seing the 'final' value of the loop-variable. But this isn't guaranteed by any means; try inserting a reasonable-duration Thread.Sleep
inside the loop, and you will see a different output.
The usual fix is to:
Capture 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 dodge the issue completely by not capturing anything:
ThreadPool.QueueUserWorkItem(obj => ((Sample)obj).RunCount(), s);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With