Threadpools - possible thread execution order prob

2019-07-18 04:42发布

问题:

I've been learning how to use the threadpools but I'm not sure that each of the threads in the pool are being executed properly and I suspect some are being executed more than once. I've cut down the code to the bare minimum and having been using Debug.WriteLine to try and work out what is going on but this produces some odd results.

My code is as follows (based on code from (WaitAll for multiple handles on a STA thread is not supported):

public void ThreadCheck()
    {
        string[] files;
        classImport Import;
        CountdownEvent done = new CountdownEvent(1);
        ManualResetEvent[] doneEvents = new ManualResetEvent[10];

        try
        {
            files = Directory.GetFiles(importDirectory, "*.ZIP");

            for (int j = 0; j < doneEvents.Length; j++)
            {
                done.AddCount();
                Import = new classImport(j, files[j], workingDirectory + @"\" + j.ToString(), doneEvents[j]);
                ThreadPool.QueueUserWorkItem(
                (state) =>
                {
                    try
                    {
                        Import.ThreadPoolCallBack(state);
                        Debug.WriteLine("Thread " + j.ToString() + " started");
                    }
                    finally
                    {
                        done.Signal();
                    }
                }, j);

            }

            done.Signal();
            done.Wait();                            
        }
        catch (Exception ex)
        {
            Debug.WriteLine("Error in ThreadCheck():\n" + ex.ToString());
        }
    }

The classImport.ThreadPoolCallBack doesn't actually do anything at the minute.

If I step through the code manually I get:

Thread 1 started Thread 2 started .... all the way to .... Thread 10 started

However, if I run it manually the Output window is filled with "Thread 10 started"

My question is: is there something wrong with my code for use of the threadpool or is the Debug.WriteLine's results being confused by the multiple threads?

回答1:

The problem is that you're using the loop variable (j) within a lambda expression.

The details of why this is a problem are quite longwinded - see Eric Lippert's blog post for details (also read part 2).

Fortunately the fix is simple: just create a new local variable inside the loop and use that within the lambda expression:

for (int j = 0; j < doneEvents.Length; j++)
{
    int localCopyOfJ = j;

    ... use localCopyOfJ within the lambda ...
}

For the rest of the loop body it's fine to use just j - it's only when it's captured by a lambda expression or anonymous method that it becomes a problem.

This is a common issue which trips up a lot of people - the C# team have considered changes to the behaviour for the foreach loop (where it really looks like you're already declaring a separate variable on each iteration), but it would cause interesting compatibility issues. (You could write C# 5 code which works fine, and with C# 4 it might compile fine but really be broken, for example.)



回答2:

Essentially the local variable j you've got there is captured by the lambda expression, resulting in the old modified closure problem. You'll have to read that post to get a broad understanding of the issue, but I can speak about some specifics in this context.

It might appear as though each thread-pool task is seeing it's own "version" of j, but it isn't. In other words, subsequent mutations to j after a task has been created is visible to the task.

When you step through your code slowly, the thread-pool executes each task before the variable has an opportunity to change, which is why you get the expected result (one value for the variable is effectively "associated" with one task). In production, this isn't the case. It appears that for your specific test run, the loop completed before any of the tasks had an opportunity to run. This is why all of the tasks happened to see the same "last" value for j (Given the time it takes to schedule a job on the thread-pool, I would imagine this output to be typical.) But this isn't guaranteed by any means; you could see pretty much any output, depending on the particular timing characteristics of the environment you're running this code on.

Fortunately, the fix is simple:

for (int j = 0; j < doneEvents.Length; j++)
{
   int jCopy = j;
   // work with jCopy instead of j

Now, each task will "own" a particular value of the loop-variable.



回答3:

the problem is that the j is a captured variable and is therefore the same capture reference is being used for each lambda expression.