Известные побочные эффекты при использовании оператора блокировки в ConcurrentDictionary.GetOrAdd? - PullRequest
1 голос
/ 16 марта 2020

У меня были некоторые проблемы при добавлении в Entity Framework DbSet из нескольких потоков из метода ConcurrentDictionary ValueFactory. Я попытался устранить эту проблему, введя оператор 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);
}

Я добавил комментарий UPPERCASE к каждой строке, которую я бы счел уместной.

Я понятия не имею, почему происходит эта ошибка. Насколько я понимаю, я только пытаюсь получить запись из dataEntryDict словаря dataEntry.ObjID ключей, которые я добавил ранее в той же итерации l oop.

До того, как я ввел две блокировки В операторах, строка, помеченная как «ЭТА ЛИНИЯ, КОТОРАЯ БЫЛА ПРОБЛЕМАТА C НА ПЕРВОМ МЕСТЕ», время от времени выдает исключение: «Сбор был изменен; операция перечисления может не выполняться». Покопавшись в коде EF, я понял, что это должно быть связано с тем, как реализован метод DbSet.Add.

Существуют ли какие-либо известные побочные эффекты при использовании оператора lock внутри ValueFactory

1 Ответ

1 голос
/ 16 марта 2020
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 и соберите результаты в коллекции. Затем обработайте результаты последовательно.

...