如何避免并行任务从ConcurrentQueue<T>重复出队及任务超量?
问题原因分析
1. 重复出队同一文件的原因
- 文件重复入队:大概率是数据库加载逻辑未正确标记文件状态,比如每次定时器触发都重复加载同一未处理文件到队列;或是解析失败的重试逻辑未加去重,将同一文件多次放回队列。
- 锁的滥用:
ConcurrentQueue本身是线程安全的,TryDequeue是原子操作,额外加锁不仅无效,还可能因锁范围不当导致线程阻塞时机异常,间接引发队列操作逻辑混乱。 Count属性的近似性:ConcurrentQueue.Count是非原子的近似值,判断Count > 0时可能队列实际已空,但仍启动任务,不过这只会导致出队失败,不会引发重复出队。
2. 任务数超过上限的原因
- 多实例并行判断任务数:
ParseQueuedTDXFiles每次事件触发都会启动独立的SetParsersTask任务,这些任务并行运行时,_activeParserTasksDict.Count < _ActiveTasksLimit的判断无锁保护,多个线程可能同时看到任务数未达上限,各自启动一批任务,导致总任务数超标。比如两个SetParsersTask同时看到任务数为0,各自启动5个任务,总任务数瞬间达到10。 - 任务记录延迟:
LaunchTDXParser中任务启动后才通过TryAdd加入字典,但SetParsersTask的任务数判断在启动任务前执行,导致启动的任务还没来得及记录到字典时,其他SetParsersTask已完成判断并继续启动新任务。 - 字典键冲突:
_activeParserTasksDict用文件名作为键,若存在重名文件,TryAdd会失败,该任务不会被记录到字典中,导致字典计数小于实际运行任务数,后续判断会继续启动新任务。
修复方案
针对重复出队问题
- 移除
ConcurrentQueue操作上的多余锁,直接使用其原生线程安全方法TryDequeue和Enqueue。 - 优化数据库加载逻辑:加载文件时标记状态(如设置为「待处理」),避免重复加载同一文件;若有重试逻辑,通过文件唯一标识(如ID+文件名)去重,防止同一文件多次入队。
针对任务数超上限问题
- 用**
SemaphoreSlim信号量**替代并发字典控制任务数,信号量原生支持并发场景下的数量限制,比手动维护字典更可靠:private readonly SemaphoreSlim _parserSemaphore = new SemaphoreSlim(5); // 对应_ActiveTasksLimit=5 private async void SetParsersTask() { while (_ConcurrentqueuedTdxFilesToParse.TryDequeue(out var fileToProcess) && !string.IsNullOrEmpty(fileToProcess?.TdxFileName)) { await _parserSemaphore.WaitAsync(); _ = Task.Run(async () => { try { await ConfigureAndStartProcessAsync(fileToProcess.TdxFileName); // 改为异步方法 } finally { _parserSemaphore.Release(); } }); } } - 修改
ParseQueuedTDXFiles逻辑,确保同一时间只有一个SetParsersTask在运行:private readonly object _taskLock = new object(); private void ParseQueuedTDXFiles() { while (_signalParseQueuedFilesEvent.WaitOne()) { lock (_taskLock) { SetParsersTask(); } } } - 将
LaunchTDXParser中的同步等待改为异步,移除冗余的任务字典维护逻辑(改用信号量后无需字典计数),提高并发效率。
内容的提问来源于stack exchange,提问作者Datboydozy
相关产品推荐
相关产品推荐

