ConcurrentDictionary缺失值问题:多线程下数据与文件计数不一致
多线程下ConcurrentDictionary丢失数据的同步问题
我编写了如下代码,AddDocument方法会被多线程访问。目前遇到的问题是:流程结束后DocumentsElement中缺失了部分值,但File.AppendAllText已正确将所有XElement值写入文本文件(文件总数为4500,是正确计数),可DocumentsElement.Count仅为4495。是否存在我忽略的同步问题?
AddDocument方法代码
ConcurrentDictionary<string, XElement> DocumentsElement = new ConcurrentDictionary<string, XElement>(); public void AddDocument(string docId) { bool added; try { var documentElement = new XElement("Document", new XAttribute("DocID", docId)); lock (lockObject) { added = DocumentsElement.TryAdd(docId, documentElement); string path = $"{_batchNumber}"; if(!Directory.Exists(path)) { Directory.CreateDirectory(path); } File.AppendAllText($"{path}/{docId}.txt", documentElement.ToString()); } } catch (Exception ex) { added = false; } if (added) { Debug.WriteLine("Document added successfully."); } else { Debug.WriteLine("Failed to add document."); } lock (lockObject) { Debug.WriteLine("From AddDocument method " + DocumentsElement.Count); } }
调用代码
private ConcurrentDictionary<string, DocumentLoadFile> loadfiles; private async Task Process(ConcurrentDictionary<long, byte> idList) { var tasks = idList.Select(async (id) => { var metadata = FetchMetadataForId(id); var loadfile = loadfiles[metadata.BatchId]; loadfile.AddDocument(metadata.DocumentId); if(metadata.HasAttachment) ProcessAttachment(metadata.AttachmentId); }); await Task.WhenAll(tasks); //后续操作 } private void ProcessAttachment(long attId) { var metadata = FetchMetadataForId(attId); var loadfile = loadfiles[metadata.BatchId]; loadfile.AddDocument(metadata.DocumentId); }
问题原因分析
TryAdd的特性导致计数不匹配:ConcurrentDictionary.TryAdd是原子操作,但它仅在目标键不存在时才会添加成功并返回true;如果键已存在,直接返回false。而你的代码中,无论TryAdd成功与否,都会执行文件写入操作。这就会出现:同一个docId被多个线程同时调用时,第一个线程成功将数据加入字典,后续线程加入失败,但都会写入文件,最终文件数量会多于字典中的条目数。- 业务逻辑存在重复ID:从调用代码来看,
Process方法遍历idList时会调用AddDocument,如果metadata.HasAttachment为true,还会在ProcessAttachment中再次调用AddDocument。如果某个附件的DocumentId和之前某个主文档的DocumentId重复,就会触发TryAdd失败但文件写入的情况,导致计数不一致。
解决方案
- 关联
TryAdd结果与文件写入:只有当TryAdd成功时才执行文件写入,避免重复写入和计数不匹配:
修改锁内代码如下:added = DocumentsElement.TryAdd(docId, documentElement); if (added) { string path = $"{_batchNumber}"; if(!Directory.Exists(path)) { Directory.CreateDirectory(path); } File.AppendAllText($"{path}/{docId}.txt", documentElement.ToString()); } - 排查重复ID来源:检查
FetchMetadataForId是否会返回重复的DocumentId,或者idList中是否存在重复ID导致重复调用AddDocument,从业务层面避免重复ID的传入。 - 优化锁的使用:
ConcurrentDictionary的TryAdd本身是线程安全的原子操作,锁的作用仅为保证TryAdd和文件写入的原子性(避免同一ID被多线程写入文件)。如果业务上允许同一ID的文件被多次写入,甚至可以去掉锁,仅通过TryAdd的结果控制字典操作。
内容的提问来源于stack exchange,提问作者LuxC
相关产品推荐
相关产品推荐

