C#线程数组Lambda初始化启动异常:为何线程未全执行且索引异常?
问题根源:闭包捕获循环变量的经典陷阱
嗨,你遇到的这个问题是C#开发里非常常见的闭包陷阱,我来给你拆解得明明白白:
为什么会出现线程id为6、执行异常的情况?
你的循环里,变量i是在循环外部声明的,Lambda表达式() => EnterSemaphore(i)捕获的是这个变量的引用,而不是当前迭代时i的具体值。
当你调用testStart[i].Start()启动线程时,线程并不会立刻执行Lambda里的逻辑——它需要等待系统调度。而此时循环还在继续,i的值会不断自增。等循环结束时,i已经变成了6(因为循环条件是i <=5,最后一次循环后i会自增到6才退出)。
那些延迟执行的线程终于运行时,拿到的i都是最终的6,这就是为什么你会看到id为6的线程,且原本预期的1-5线程逻辑混乱的原因。
为什么另外两种方式能正常工作?
- 直接用
new Thread(EnterSemaphore).Start(i):这里Start(i)是把当前迭代的i值作为参数传递给线程,线程启动时会拿到这个值的副本,而非引用。每个线程都有独立的参数值,自然不受后续循环影响。 - 加
testStart[i].Join():Join()会让主线程等待当前线程完全执行完毕后再进入下一次循环。这意味着每次循环的i值还没被更新,线程就已经执行了Lambda代码,所以能拿到正确的i值。
怎么修复线程数组的启动方式?
解决方法很简单:在循环内部创建一个局部变量,保存当前i的值,让Lambda捕获这个局部变量。这样每个Lambda都会捕获一个独立的变量,对应循环当前迭代的i值。修改后的代码如下:
public SemaphoreLock() { Thread[] testStart = new Thread[10]; for (int i = 1; i <= 5; i++) { // 创建局部变量保存当前i的值,避免闭包捕获同一个引用 int localId = i; testStart[i] = new Thread(() => EnterSemaphore(localId)); lock (StartLocker) testStart[i].Start(); } }
补充一句:你代码里的lock (StartLocker)对这个问题没有帮助,它只是保证线程启动操作的原子性,无法改变Lambda捕获变量的本质。
内容的提问来源于stack exchange,提问作者Wouter




