我一直在学习如何使用线程池,但我不确定池中的每个线程是否正确执行,我怀疑有些线程被执行多次。我已经将代码减少到最低限度并且一直在使用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的结果被多个线程搞糊涂了?
答案 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表达式都使用相同的捕获引用。