嵌套线程场景下for循环的索引溢出问题求助
多线程循环索引溢出问题排查
问题场景
开发WinForm网站性能测试工具,支持串行/并行执行请求,串行执行正常,但并行执行时出现for循环索引溢出。采用常规for循环结合Task.Run()执行嵌套Action,暂未使用Parallel.For。
问题代码
private void Run() { ConcurrentBag<long> callTimes = new ConcurrentBag<long>(); int httpErrors = 0; int progress = 0; string uri = txtUrl.Text ?? string.Empty; if (string.IsNullOrWhiteSpace(uri)) return; Func<List<string>,int,long> testCall = (p,i) => { try { using (var client = new HttpClient()) { Stopwatch timer = new Stopwatch(); timer.Start(); string actualUrl = string.Format(uri, p.ToArray()); var getTask = client.GetAsync(actualUrl); getTask.Wait(); timer.Stop(); var result = getTask.Result; if (result == null || (int)result.StatusCode >= 400) { txtErrors.ThreadSafeAppendText($"Connection error {(result?.StatusCode.ToString() ?? "NULL")}'\r\n"); Interlocked.Increment(ref httpErrors); } return timer.ElapsedMilliseconds; } } catch ( Exception actionErr) { txtErrors.ThreadSafeAppendText($"Error while execution callAction {i} with parameters '{string.Join(", " , p)}' : \r\n" + actionErr.Message); } return -1; }; try { List<List<string>> parameters = this.ParseParameters(); int parametersCount = parameters.Count; int executions = (int)updRequests.Value; //预计算随机数,避免并行时的随机问题 Random rng = new Random(); List<int> randoms = new List<int>(); for (int i = 0; i < executions; i++) randoms.Add(rng.Next(0, parametersCount)); for ( int index = 0; index < executions; index++) { Action parallelAction = () => { int currentIndex = index; List<string> currentParameter = parameters[randoms[currentIndex] % parametersCount]; //<<--- 此处出现currentIndex >= executions的溢出 callTimes.Add(testCall(currentParameter, currentIndex)); Interlocked.Increment(ref progress); if (progress % 10 == 0) prbProgress.ThreadSafeAction(this.RefreshProgressBar, progress, executions); }; if (chkParallelExecution.Checked) Task.Run(parallelAction); else parallelAction(); } this.Reporting(callTimes, httpErrors); } catch (Exception err) { txtErrors.ThreadSafeAppendText($"Error while running stress test : \r\n" + err.Message); } }
异常原因分析
问题核心是闭包对循环变量index的引用捕获机制:
- 主线程的循环执行速度远快于
Task.Run()启动的后台任务,循环很快就会走完,index最终会变成executions(循环终止条件是index < executions,结束时index等于executions)。 - 你在Action内部写的
int currentIndex = index;并没有解决问题,因为这个赋值操作是在后台Task执行时才会触发,此时主线程的循环早已完成,index的值已经超出了合法范围。 - 所有后台Task捕获的是同一个
index变量的引用,而非循环迭代时的当前值,当Task最终执行时,读取到的index可能已经是executions,导致currentIndex >= executions的溢出错误。
解决方案
在循环内部创建局部临时变量保存当前index的值,让闭包捕获这个独立的临时变量(每个循环迭代都会生成一个新的临时变量),修改循环部分代码如下:
for ( int index = 0; index < executions; index++) { // 关键:在循环内部创建局部变量,保存当前迭代的index值 int loopIndex = index; Action parallelAction = () => { int currentIndex = loopIndex; List<string> currentParameter = parameters[randoms[currentIndex] % parametersCount]; callTimes.Add(testCall(currentParameter, currentIndex)); Interlocked.Increment(ref progress); if (progress % 10 == 0) prbProgress.ThreadSafeAction(this.RefreshProgressBar, progress, executions); }; if (chkParallelExecution.Checked) Task.Run(parallelAction); else parallelAction(); }
每个Task捕获的是独立的loopIndex变量,其值固定为对应循环迭代时的index,不会被后续循环修改,彻底避免索引溢出问题。
内容的提问来源于stack exchange,提问作者Skary
相关产品推荐
相关产品推荐

