Threadpools - 可能的线程执行顺序问题

时间:2010-12-17 10:24:17

标签: c# multithreading loops lambda threadpool

我一直在学习如何使用线程池,但我不确定池中的每个线程是否正确执行,我怀疑有些线程被执行多次。我已经将代码减少到最低限度并且一直在使用Debug.WriteLine来尝试找出正在发生的事情,但这会产生一些奇怪的结果。

我的代码如下(基于(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());
        }
    }

classImport.ThreadPoolCallBack实际上并没有做任何事情。

如果我手动单步执行代码,我会得到:

线程1开始了 线程2开始了 ....一路...... 线程10开始

但是,如果我手动运行它,“输出”窗口将填充“Thread 10 started”

我的问题是:我的代码使用了线程池有什么问题,或者Debug.WriteLine的结果被多个线程搞糊涂了?

3 个答案:

答案 0 :(得分:3)

问题是你在lambda表达式中使用循环变量(j)。

这是一个问题的详细信息非常漫长 - 有关详细信息,请参阅Eric Lippert's blog post(另请阅读part 2)。

幸运的是,修复很简单:只需在循环中创建一个新的局部变量,并在lambda表达式中使用它:

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

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

对于循环体的其余部分,只使用j就可以了 - 只有当它被lambda表达式或匿名方法捕获时才会成为问题。

这是一个常见的问题,让很多人感到高兴 - C#团队已经考虑过对foreach循环的行为进行了更改(其中真的看起来你已经是在每次迭代时声明一个单独的变量),但这会引起有趣的兼容性问题。 (您可以编写C#5代码,该代码可以正常工作,而使用C#4可能编译正常,但实际上会被破坏,例如。)

答案 1 :(得分:2)

基本上,你所到达的局部变量j是由lambda表达式捕获,导致旧的modified closure问题。您必须阅读该帖子才能广泛了解该问题,但我可以在此背景下谈论一些细节

它可能出现,好像每个线程池任务都看到它自己的j“版本”,但事实并非如此。换句话说,在创建任务后,后续突变到j 对任务可见。

当您慢慢单步执行代码时,线程池会在变量有机会更改之前执行每个任务,这就是您获得预期结果的原因(变量的一个值)与一个任务“关联”。在生产中,情况并非如此。对于特定的测试运行,似乎在任何任务有机会运行之前,循环已完成。这就是为什么所有任务都发生了j相同的“last”值的原因(考虑到在线程池上调度作业所花费的时间,我会想象这个输出是典型的。)但这并不能保证任何方式;您可以看到几乎任何输出,具体取决于您运行此代码的环境的特定时序特征。

幸运的是,修复很简单:

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

现在,每个任务都将“拥有”循环变量的特定值。

答案 2 :(得分:1)

问题是j是一个捕获的变量,因此每个lambda表达式都使用相同的捕获引用。