【问题标题】:Known side effects when using lock statement in ConcurrentDictionary.GetOrAdd?在 ConcurrentDictionary.GetOrAdd 中使用 lock 语句时的已知副作用?
【发布时间】:2020-03-16 14:10:35
【问题描述】:

ConcurrentDictionary ValueFactory 方法内部的多个线程添加到实体框架DbSet 时遇到了一些问题。我试图通过引入lock 声明来消除这个问题。不过,这似乎有一些奇怪的副作用。在一些罕见和随机的情况下,我的代码会抛出 KeyNotFoundException,即使编程应该防止这种情况发生。我想我在监督一些事情。

using (ESBClient client = new ESBClient()) { // WCF SERVICE

    client.Open();

    // Limit the maximum number of parallel requests
    var esbLimiter = new SemaphoreSlim(4);

    ConcurrentDictionary<string, DataEntry> dataEntryDict  = new ConcurrentDictionary<string, DataEntry>(
        await db.DataEntries
            .Where(de => allObjIDs.Contains(de.PAObjID))
            .IncludeOptimized(de => de.WorkSchedules)
            .ToDictionaryAsync(a => a.PAObjID, a => a)
    );


    // Get WorkOrderDataSet02 for each data entry number
    await Task.WhenAll(allDataEntryNumbers.Batch(20).Select(async workOrderBatch => {
        await esbLimiter.WaitAsync();

        Debug.WriteLine($"Starting for new batch after {s.ElapsedMilliseconds} with parallel {esbLimiter.CurrentCount}");

        try {
            int retryCounter = 0;
            getWorkOrderDataSet02Response gwoResp;

            retryCurrentWorkOrderDataSetResp:
            try {
                gwoResp = await client.getWorkOrderDataSet02Async(
                    new getWorkOrderDataSet02Request(
                        "?",
                        companyGroup.Key,
                        string.Join(",", workOrderBatch.Select(wob => wob.DataEntryNumber)),
                        "WNTREIB",
                        "?",
                        "act,sales",
                        "D"
                    )
                );

            } catch (System.ServiceModel.CommunicationException ex) {
                // Retry up to 3 times before finally crashing
                if (retryCounter++ < 3) {
                    await HandleServiceRetryError("getWorkOrderDataSet02Async", retryCounter, s.ElapsedMilliseconds, ex);
                    goto retryCurrentWorkOrderDataSetResp;
                } else
                    throw;
            }

            // Iterate over all work orders returned by the ESB
            foreach (dsyWorkOrder01TtyWorkOrder currDetail in gwoResp.dsyWorkOrder01) { // dsyWorkOrder01 IS AN ARRAY OF OBJECTS. IT COMES FROM A WCF CALL. PAObjID IS UNIQUE.
                // Get or create element
                DataEntry currentEntry = dataEntryDict.GetOrAdd(
                    currDetail.Obj,
                    key => {
                        DataEntry newDe = new DataEntry();
                        lock (db.DataEntries) { // I INTRODUCED THOSE LOCK STATEMENTS
                            db.DataEntries.Add(newDe); // THIS IS THE LINE THAT WAS PROBLEMATIC IN THE FIRST PLACE
                        }
                        return newDe;
                    }
                );

                // Set regular fields
                currentEntry.ApplyTtyWorkOrder(currDetail, resourceDict); // THIS METHOD APPLIES THE PAObjID PROPERTY
            }

            // Delete all elements, that were not provided by the service anymore
            lock(db.DataEntries) { 
                workOrderBatch
                    .Where(wob => !gwoResp.dsyWorkOrder01
                        .Where(wo => wo.DataEntryNumber.HasValue)
                        .Select(wo => wo.DataEntryNumber.Value)
                        .Contains(wob.DataEntryNumber)
                    )
                    .ToArray()
                    .ForEach(dataEntry => {
                        try {
                            db.DataEntries.Remove(dataEntryDict[dataEntry.ObjID]); // THIS LINE THROWS THE KeyNotFoundException
                        } catch (Exception ex) {
                            throw new Exception($"Key {dataEntry.ObjID} not in list.", ex);
                        }

                    });
            }

            // Update progress
            progress.Report(.1f + totalSteps * Interlocked.Increment(ref currentStep) * .8f);

        } finally {
            Debug.WriteLine($"Finished for batch after {s.ElapsedMilliseconds} with parallel {esbLimiter.CurrentCount}");
            esbLimiter.Release();
        }

    }));
}

// HERE'S THE APPLY METHOD
public void ApplyTtyWorkOrder(dsyWorkOrder01TtyWorkOrder src, Dictionary<(string Name, byte ResourceType), int> resourceDict) {
    Deleted = false;

    DataEntryNumber = src.DataEntryNumber.Value;
    PAObjID = src.Obj; // PAObjID IS APPLIED HERE
    IsHeader = src.IsHeader;
    Pieces = Convert.ToInt16(src.ProductionQty);
    PartNo = src.Article;
    JobNo = src.WorkOrder;
    StartDate = src.StartDate;
    FinishDate = src.EndDate;
    FinishedPA = src.WorkOrderStatus == "R";

    // Update methods
    UpdateFromTtyCustomer(src.ttyCustomer?.FirstOrDefault());
    UpdateFromPart(src.ttyPart?.FirstOrDefault());
    UpdateFromSalesDocHeader(src.ttySalesDocHeader?.FirstOrDefault());
    UpdateWorkSchedules(src.ttyWorkOrderActivity, resourceDict);
}

我在我认为相关的每一行都添加了一个大写注释。

我不知道为什么会发生这个错误。据我了解,我只尝试从 dataEntryDict 字典 dataEntry.ObjID 键中获取一个条目,这些键是我之前在循环的同一迭代中添加的。

在我介绍这两个锁语句之前,标有“这是第一个地方有问题的行”的行偶尔会抛出异常:“集合已修改;枚举操作可能无法执行。”深挖EF的代码后,我意识到这应该与DbSet.Add方法的实现方式有关。

ValueFactory 中使用lock 语句时是否有任何已知的副作用?

【问题讨论】:

    标签: c# entity-framework concurrentdictionary


    【解决方案1】:
    lock (db.DataEntries) { // I INTRODUCED THOSE LOCK STATEMENTS
        db.DataEntries.Add(newDe); // THIS IS THE LINE THAT WAS PROBLEMATIC IN THE FIRST PLACE
    }
    

    问题在于db.DataEntries 不是线程安全的集合,而是被多个线程同时访问。所有 EF 对象都不是线程安全的。

    在这里使用锁定似乎是一个很好的解决方案。确保你抓住了所有的地方。

    通常最好将并发部分与顺序部分分开。 only 使client.getWorkOrderDataSet02Async 调用并发并将结果收集到一个集合中。然后,依次处理结果。

    【讨论】:

    • 好的,看来我走对了。但是你能告诉我,为什么加锁会引入 KeyNotFound 问题?锁不会阻塞线程吗?对我来说,这整件事有种族条件的恶心气味。我觉得命令dataEntryDict[dataEntry.ObjID] 是在一些非常罕见的情况下实际将元素添加到字典之前执行的,这可能吗?也很奇怪,我无法在本地机器上重现该问题,也无法在测试环境中重现。它只发生在生产服务器上。
    • 我承认我没有完全阅读代码(因为它很多)。我真的建议您尽量减少并发部分。很可能,这个错误就消失了,代码变得更好了。我现在试图找到一个错误,但没有成功。但我也不明白这段代码应该如何安全。是什么确保dataEntryDict 始终包含正在查找的任何 ID 的对象?我没有看到任何这样的保证。
    • 该代码用于同步两个系统(另一个系统是主导系统)。基本上有两种方法可以将条目添加到该列表中。首先,我收集本地数据库中的所有数据(不是帖子)。这也被添加到该字典中。然后我得到源系统中所有数据条目编号的列表。 allDataEntryNumbers 是所有不同 DEN 的列表。因此,dataEntryDict 应该在引发异常的位置包含来自数据库或 WCF 服务的相应条目。在我的整个代码中,我从不编造任何对象 ID。
    • PS:对象ID和数据条目号是1对1的关系。
    • 可以有重复吗?也许同一个条目被删除两次。一般来说,我会通过将源和目标构建为列表来解决数据同步问题,然后对它们使用集合操作,例如除外或相交。我也为自己编写了一个完整的连接辅助方法。这个问题老是出现。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多