Parallel.ForEachAsync生成SSRS报表时文件名与内容不匹配问题排查
问题描述
为提升SSRS报表生成速度,我在.NET 8中使用Parallel.ForEachAsync开启4线程并行执行流程:查询数据库→请求SSRS生成报表→写入硬盘。但部分文件出现文件名与内容不匹配的情况,文件名基于报表请求参数生成。单文件生成完全正常,推测是并行线程导致的问题。此前项目中类似的PDF报表并行生成逻辑无此异常,仅当前生成EXCELOPENXML格式报表时出现问题。附上核心代码片段,希望排查原因并获取解决方案或优化建议。
核心代码片段
并行处理逻辑
public async Task ProcessReportOnParallelAsync( PocoClass pocoObj ) { List<People> people = (await this.PeopleRepository.RetrieveAllInvolved( pocoObj.Params )).ToList(); await Parallel.ForEachAsync( people, new ParallelOptions{ MaxDegreeOfParallelism = 4 }, async ( personPoco, token ) => { var personDetail = await this.PersonDetailsRepository( personPoco.Id, personPoco.CompanyId ); if( personDetail.ShouldGenerateReport ) { await Report.GenerateAsync( personPoco ); } }); }
报表生成与写入逻辑
public async static Task GenerateAsync( PocoClass pocoObj ) { //setup all SSRS object configuration to request the report generation var rsExec = new ReportExecutionServiceSoapClient( httpBinding, new EndpointAddress(SSRSReportExecutionUrl)); // ... // Finally await rsExec.SetExecutionParameterAsync( null, null, reportParams, "en-us" ); const string deviceInfo = @"<DeviceInfo><Toolbar>False</Toolbar><DeviceInfo>"; var response = await rsExec.RenderAsync( new RenderRequest(null, null, "EXCELOPENXML", deviceInfo)); var byteResults = response.Result; string filename = $@"{pocoObj.Id}_{pocoObj.Name}_{pocoObj.Company}"; string filepath = $@"{this.FolderPathForFileGeneration}\{filename}"; var fs = new FileStream(filepath, FileMode.Create, FileAccess.ReadWrite); await fs.WriteAsync( byteResults); fs.Close(); }
原因分析
- SSRS客户端线程安全问题:
ReportExecutionServiceSoapClient并非线程安全组件,并行场景下若多个线程共享实例(或实例创建逻辑未隔离),会出现参数串扰。EXCELOPENXML格式的渲染流程比PDF更复杂,参数上下文更容易被覆盖,导致渲染内容与当前线程的pocoObj不匹配。 - 文件写入的潜在竞态:虽然文件名基于参数生成,但如果存在重名(比如
Id+Name+Company组合重复),FileMode.Create会直接覆盖文件;同时手动Close()流可能因异步操作未完成导致资源泄漏,间接引发内容写入异常。 - 参数传递未隔离:若
reportParams是外部共享的集合,并行线程会修改同一对象,导致SSRS接收的参数与当前pocoObj不对应。
解决方案与优化建议
1. 强制SSRS客户端实例线程隔离
在GenerateAsync内部每次创建独立的ReportExecutionServiceSoapClient实例,用using确保资源及时释放,避免线程间状态串扰:
public async static Task GenerateAsync( PocoClass pocoObj ) { // 每次生成报表创建独立客户端,彻底隔离线程状态 using var rsExec = new ReportExecutionServiceSoapClient( httpBinding, new EndpointAddress(SSRSReportExecutionUrl)); // ... 其他配置代码 // 修复deviceInfo标签未闭合的问题 const string deviceInfo = @"<DeviceInfo><Toolbar>False</Toolbar></DeviceInfo>"; var response = await rsExec.RenderAsync( new RenderRequest(null, null, "EXCELOPENXML", deviceInfo)); var byteResults = response.Result; // ... 文件名与写入逻辑 }
2. 优化文件写入逻辑,避免覆盖与泄漏
- 用
Path.Combine替代字符串拼接,避免路径格式错误; - 用
FileMode.CreateNew替代Create,文件已存在时抛出异常,及时发现重名问题; - 用
await using自动管理流资源,无需手动Close():
// 加上文件后缀,明确标识EXCEL文件 string filename = $"{pocoObj.Id}_{pocoObj.Name}_{pocoObj.Company}.xlsx"; string filepath = Path.Combine(this.FolderPathForFileGeneration, filename); await using var fs = new FileStream(filepath, FileMode.CreateNew, FileAccess.Write); await fs.WriteAsync(byteResults);
3. 确保参数集合独立
在GenerateAsync内部创建专属的reportParams集合,避免共享外部对象:
// 基于当前pocoObj创建独立参数集合 var reportParams = new List<ParameterValue> { new ParameterValue { Name = "PersonId", Value = pocoObj.Id.ToString() }, new ParameterValue { Name = "CompanyId", Value = pocoObj.CompanyId.ToString() } // 其他报表参数 }; await rsExec.SetExecutionParameterAsync( null, null, reportParams, "en-us" );
4. 灵活控制并发(可选)
若SSRS服务器压力较大,可改用Task.WhenAll手动控制并发数,比Parallel.ForEachAsync更灵活:
public async Task ProcessReportOnParallelAsync( PocoClass pocoObj ) { List<People> people = (await this.PeopleRepository.RetrieveAllInvolved( pocoObj.Params )).ToList(); var tasks = new List<Task>(); foreach (var personPoco in people) { var personDetail = await this.PersonDetailsRepository( personPoco.Id, personPoco.CompanyId ); if( personDetail.ShouldGenerateReport ) { tasks.Add(Report.GenerateAsync( personPoco )); // 每积累4个任务就等待执行完成,控制并发数 if(tasks.Count >= 4) { await Task.WhenAll(tasks); tasks.Clear(); } } } // 等待剩余任务完成 await Task.WhenAll(tasks); }
内容的提问来源于stack exchange,提问作者Eduardo Dorneles
相关产品推荐
相关产品推荐

