Обнаружение проблемы синхронизации в многопоточном сервисе

Проблема — совместное изменение обычного Dictionary без синхронизации. ContainsKey и Add не атомарны; чтение словаря одновременно с записью тоже небезопасно.

Короткий ответ

Что ответить на собеседовании

Проблема — совместное изменение обычного Dictionary без синхронизации. ContainsKey и Add не атомарны; чтение словаря одновременно с записью тоже небезопасно.

Подробный разбор

Ответ с пояснениями

Условие

Дан класс FlatteningService, содержащий несколько полей‑коллекций, в частности Dictionary<string, IList<FlattenLocationFilter>> locationFilters. Внутри метода MakeFlatten происходит чтение и запись в эту словарную структуру. При одновременных вызовах метода из десяти потоков могут возникать исключения типа KeyNotFoundException и другие связанные с доступом к словарю. Важно понять, почему при проверке наличия ключа через ContainsKey и последующем добавлении через Add появляется ошибка «ключ уже существует», а последующее обращение к locationFilters[locationFilter] приводит к KeyNotFoundException. Найдите и опишите источник проблемы.

public class FlatteningService
{
    private readonly Dictionary<string, IList<FlattenLocationFilter>> locationFilters;

    public PromoFlatteningService()
    {
        locationFilters = new Dictionary<string, IList<FlattenLocationFilter>>();
    }

    public IEnumerable<FlattenPromo> MakeFlatten(IEnumerable<OraclePromo> items)
    {
        var itemsList = items.ToLookup(x => x.PromoCode);

        var flatList = new List<FlattenPromo>();

        foreach (var src in itemsList.Single())
        {
            var promoCode = $"{src.PromoCode}";

            const string locationFilter = "Auchan";
            if (!locationFilters.ContainsKey(locationFilter))
            {
                // выходит ошибка, что ключ уже существует
                locationFilters.Add(locationFilter, new List<FlattenLocationFilter>
                {
                    new()
                    {
                        FilterCode = locationFilter,
                        FilterOperator = "AND",
                        CriteriaCode = "Location",
                        CriteriaOperator = "OR",
                        ExpectedValue = "Auchan",
                        ExpectedOperator = "EQ",
                        FilterDescription = "none",
                        LocationPropertyCode = "none"
                    }
                });

                flatlist.Add(new FlattenPromo
        {
            PromoCode = promoCode,
            PromoBegin = src.StartDate.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc),
            PromoEnd = src.EndDate.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc),
            LocationFilterCode = locationFilter,
            LocationFilter = locationFilters[locationFilter], // выходит ошибка keyNotFoundException
            ProductCode = src.ProductId,
            ProductDiscountPercent = src.DiscountPercent,
            ProductPlanQuantity = src.PlanSum,
        });
            }
        }
    }
}

Ответ

Проблема — совместное изменение обычного Dictionary без синхронизации. ContainsKey и Add не атомарны; чтение словаря одновременно с записью тоже небезопасно.

Два потока могут одновременно увидеть отсутствие ключа, после чего второй Add встретит уже добавленный ключ. Все обращения нужно защитить одним lock либо использовать ConcurrentDictionary и возвращаемое GetOrAdd значение. Фабрика GetOrAdd может исполняться несколько раз, поэтому не должна иметь побочных эффектов; изменяемый список внутри словаря требует отдельной защиты.

Если фильтр одинаков для всех вызовов, проще создать его один раз и больше не менять. KeyNotFoundException нельзя однозначно объяснить только парой ContainsKey/Add: нужны стек и остальные обращения к словарю. В примере также неверное имя конструктора, flatlist вместо flatList, нет return, а добавление результата ошибочно находится внутри условия создания фильтра.

Источник: learn.microsoft.com.

ИИ-помощник для собеседований

Хочешь уверенно проходить собеседования?

Попробуй ИИ-помощник для собеседований: слышит вас и собеседника, анализирует экран, подсказывает ответы в реальном времени, работает без VPN и не попадает в захват экрана.

Подробнее