You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
}
原因分析
  1. SSRS客户端线程安全问题:ReportExecutionServiceSoapClient并非线程安全组件,并行场景下若多个线程共享实例(或实例创建逻辑未隔离),会出现参数串扰。EXCELOPENXML格式的渲染流程比PDF更复杂,参数上下文更容易被覆盖,导致渲染内容与当前线程的pocoObj不匹配。
  2. 文件写入的潜在竞态:虽然文件名基于参数生成,但如果存在重名(比如Id+Name+Company组合重复),FileMode.Create会直接覆盖文件;同时手动Close()流可能因异步操作未完成导致资源泄漏,间接引发内容写入异常。
  3. 参数传递未隔离:若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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 09:25:20