Проблема — совместное изменение обычного 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.