C# 线程池-可能的线程执行顺序问题

C# 线程池-可能的线程执行顺序问题,c#,multithreading,loops,lambda,threadpool,C#,Multithreading,Loops,Lambda,Threadpool,我一直在学习如何使用线程池,但我不确定池中的每个线程是否都被正确执行,我怀疑有些线程被执行了不止一次。我已经将代码缩减到最低限度,并一直在使用Debug.WriteLine来尝试解决问题,但这会产生一些奇怪的结果 我的代码如下(基于()中的代码): public void ThreadCheck() { 字符串[]文件; 分类进口; CountdownEvent done=新的CountdownEvent(1); ManualResetEvent[]DoneeEvents=新的ManualRes

我一直在学习如何使用线程池,但我不确定池中的每个线程是否都被正确执行,我怀疑有些线程被执行了不止一次。我已经将代码缩减到最低限度,并一直在使用Debug.WriteLine来尝试解决问题,但这会产生一些奇怪的结果

我的代码如下(基于()中的代码):

public void ThreadCheck()
{
字符串[]文件;
分类进口;
CountdownEvent done=新的CountdownEvent(1);
ManualResetEvent[]DoneeEvents=新的ManualResetEvent[10];
尝试
{
files=Directory.GetFiles(importDirectory,*.ZIP);
对于(int j=0;j
{
尝试
{
Import.ThreadPoolCallBack(状态);
Debug.WriteLine(“线程”+j.ToString()+“已启动”);
}
最后
{
完成。信号();
}
},j);
}
完成。信号();
完成。等待();
}
捕获(例外情况除外)
{
Debug.WriteLine(“ThreadCheck()中的错误:\n”+ex.ToString());
}
}
classImport.ThreadPoolCallBack此时实际上不执行任何操作

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

线程1已启动 线程2已启动 ……一直到。。。。 线程10已启动

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


我的问题是:我使用线程池的代码有什么问题吗?或者是调试。WriteLine的结果被多个线程混淆了吗?

本质上,你在那里得到的局部变量
j
被lambda表达式捕获,导致了老问题。你必须阅读那篇文章才能获得广泛的理解我对这个问题的看法是肯定的,但我可以在这方面谈一些细节

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

当您缓慢地遍历代码时,线程池会在变量有机会更改之前执行每个任务,这就是为什么您会得到预期的结果(变量的一个值实际上与一个任务“关联”)。在生产环境中,情况并非如此。对于特定的测试运行,循环似乎在任何任务有机会运行之前完成。这就是为什么所有任务都碰巧看到相同的
j
的“last”值(考虑到在线程池上安排作业所需的时间,我认为此输出是典型的)但这并不能得到任何保证;您可以看到几乎任何输出,这取决于您运行此代码的环境的特定定时特征

幸运的是,解决方法很简单:

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

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

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

问题在于在lambda表达式中使用循环变量(
j

关于为什么这是一个问题的详细信息非常冗长-请参阅了解详细信息(另请参阅)

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

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

    ... use localCopyOfJ within the lambda ...
}
for(int j=0;j
对于循环体的其余部分,可以只使用
j
——只有当它被lambda表达式或匿名方法捕获时,它才会成为问题


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

谢谢你的解释,乔恩,这现在说得通了。在我写这本书的时候,也谢谢你的深度书,这是我多年来读过的最好的一本。多亏了这本书,我很喜欢从.Net第2版到第4版。@GrandMasterFlush:你真是太好了-谢谢你给我脸上一个微笑:)谢谢Ani,我还在学习lambda的所有东西,还没有意识到这会是一个问题。@GrandMasterFlush:你知道,再看一遍代码,我看到你正在将
j
传递给该方法。所以我想你可以这样做:
“Thread”+state.ToString()
相反。通常,最好的解决方案是不捕获,而不是尝试对闭包语义进行推理。感谢指针Ani。我还更改了代码,以便在for循环中声明导入,而不是在方法中声明导入。这解决了我遇到的所有classImport对象都使用相同数据运行的问题。
for (int j = 0; j < doneEvents.Length; j++)
{
    int localCopyOfJ = j;

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