使用C# Task多线程处理大PDF文件文本提取异常问题
问题分析与解决方案
核心问题排查
1. 闭包变量捕获陷阱
循环中直接在Task.Run里引用startpage变量,由于闭包捕获的是变量引用而非当前值,所有任务最终可能拿到的都是循环结束时的startpage值,导致大量任务重复处理同一页面范围,或跳过大量页面。
2. 页面范围计算错误
循环步长startpage = startpage + num + 1会导致页面区间遗漏:
- 若总页数不是
num+1的整数倍,末尾的若干页面会被跳过; - 当
startpage+num超过总页数时,ProcessSinglePDF会处理不存在的页面,引发隐性错误。
3. PdfReader线程不安全
iTextSharp的PdfReader实例不支持多线程并发访问,多个任务共用同一个reader会导致内部状态混乱,出现读取失败、数据丢失甚至任务异常静默终止。
4. 异常未处理
未捕获任务执行中的异常,若某个任务因页面不存在、数据库错误等抛出异常,await时未处理会导致程序看似正常完成,但实际任务已失败。
修正后的代码
var processTask = new List<Task>(); var pdfDir = Path.Combine(sourcePath, "PDFs"); Directory.CreateDirectory(pdfDir); // 确保输出目录存在 int totalPages = reader.NumberOfPages; for (int startpage = 1; startpage <= totalPages; startpage += num) { // 复制循环变量到局部变量,规避闭包陷阱 int currentStart = startpage; // 计算当前区间的结束页,避免超过总页数 int currentEnd = Math.Min(currentStart + num - 1, totalPages); processTask.Add(Task.Run(async () => { // 每个任务创建独立的PdfReader实例,保证线程安全 using (var taskReader = new PdfReader(fileInfo.FullName)) { var outputPath = Path.Combine(pdfDir, $"{currentStart}.pdf"); await ProccesSinglePDF( taskReader, outputPath, currentStart, currentEnd, new FileInfo(outputPath), searchText); } })); } // 等待所有任务完成,并统一捕获异常 try { await Task.WhenAll(processTask); } catch (AggregateException ex) { foreach (var innerEx in ex.InnerExceptions) { Console.WriteLine($"任务执行失败: {innerEx.Message}"); } }
额外优化建议
- 数据库并发控制:多个任务写数据库时,依赖数据库连接池(默认开启)即可,若存在冲突可考虑批量写入或事务控制;
- 并发数限制:用
SemaphoreSlim限制并发任务数,防止CPU/内存过载:var semaphore = new SemaphoreSlim(Environment.ProcessorCount * 2); // 并发数设为CPU核心数的2倍 // 在任务内部先获取信号量 await semaphore.WaitAsync(); try { // 任务逻辑 } finally { semaphore.Release(); } - 日志记录:在
ProcessSinglePDF中添加页面范围、处理结果的日志,方便排查问题。
内容的提问来源于stack exchange,提问作者Elmer A. Chacon
相关产品推荐
相关产品推荐

