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

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失败但文件写入的情况,导致计数不一致。

解决方案

  1. 关联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());
    }
    
  2. 排查重复ID来源:检查FetchMetadataForId是否会返回重复的DocumentId,或者idList中是否存在重复ID导致重复调用AddDocument,从业务层面避免重复ID的传入。
  3. 优化锁的使用:ConcurrentDictionary的TryAdd本身是线程安全的原子操作,锁的作用仅为保证TryAdd和文件写入的原子性(避免同一ID被多线程写入文件)。如果业务上允许同一ID的文件被多次写入,甚至可以去掉锁,仅通过TryAdd的结果控制字典操作。

内容的提问来源于stack exchange,提问作者LuxC

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 22:43:05