Какие проблемы возникают при многопоточном доступе к синглтону с коллекцией и как их решать
Что проверяют
Различаете ли вы две проблемы: потокобезопасное создание синглтона и потокобезопасную работу с его состоянием. В ASP.NET Core это особенно важно, сервис из AddSingleton обслуживает все запросы сразу. Следом обычно спрашивают, чем ConcurrentDictionary лучше lock и где он всё равно не спасает.
Как отвечать
- создание: вопрос закрыт
Lazy<T>(режим по умолчаниюExecutionAndPublicationгарантирует одну инициализацию) или статическим readonly-полем, а в ASP.NET Core экземпляр создаёт DI-контейнер. Ручной double-checked locking писать незачем; - гонка на коллекции:
List<T>иDictionary<TKey, TValue>не потокобезопасны. ПараллельныеAddтеряют элементы или портят внутреннее состояние, а перебор во время записи падает сInvalidOperationException; - составные операции: даже с
ConcurrentDictionaryпара «проверилContainsKey, потом добавил» остаётся гонкой. Нужны атомарныеGetOrAdd,AddOrUpdate,TryRemove. Фабрика вGetOrAddможет вызваться несколько раз, поэтому дорогие значения кладут черезLazy<T>; - утечка ссылки: метод, который возвращает саму коллекцию, выносит её из-под
lock. Отдавайте копию или неизменяемый снимок:ImmutableList<T>,FrozenDictionaryдля редко меняющихся данных; - async: внутри
lockнельзяawait, компилятор не даст. Для асинхронного кода берутSemaphoreSlim(1, 1)иWaitAsync.
Инструмент выбирают по профилю нагрузки. Много чтений и редкие записи: ReaderWriterLockSlim или атомарная замена ссылки на неизменяемую коллекцию через Interlocked.Exchange. Частые записи по ключам: ConcurrentDictionary.
Где спотыкаются
Считают, что Lazy<T> делает потокобезопасным и содержимое. Он защищает только момент создания, дальше коллекция открыта всем потокам.
Блокируют на this или на публичном объекте. Внешний код может взять тот же lock, и получается взаимоблокировка, которую трудно найти. Нужен приватный readonly object.
Хранят в синглтоне данные конкретного запроса или внедряют в него scoped-зависимость вроде DbContext. Один пользователь видит состояние другого, а DbContext не рассчитан на параллельную работу и падает при одновременных запросах.