Однако до сих пор при тестировании я не видел никаких исключений при вызове его из нескольких потоков одновременно.
Это потокобезопасно или мне просто повезло? Если это потокобезопасный, то почему?
Тебе повезло. Эти типы ошибок с потоками так просто сделать, потому что тестирование может дать вам ложное чувство безопасности, что вы все сделали правильно.
Оказывается, Dictionary<TKey, TValue>
не является потокобезопасным, когда у вас есть несколько писателей. В документации прямо говорится:
A Dictionary<TKey, TValue>
может поддерживать несколько считывателей одновременно, если коллекция не изменена. Тем не менее, перечисление в коллекции по сути не является потокобезопасной процедурой. В редком случае, когда перечисление конкурирует с доступом для записи, коллекция должна быть заблокирована в течение всего перечисления. Чтобы разрешить доступ к коллекции из нескольких потоков для чтения и записи, необходимо реализовать собственную синхронизацию.
В качестве альтернативы используйте ConcurrentDictionary
. Однако вы все равно должны написать правильный код (см. Примечание ниже).
В дополнение к отсутствию поточной безопасности с Dictionary<TKey, TValue>
, которого вам посчастливилось избежать, ваш код опасно ошибочен. Вот как вы можете получить ошибку с вашим кодом:
static void IncreaseValue(int keyId, int adjustment) {
if (!KeyValueDictionary.ContainsKey(keyId)) {
// A
KeyValueDictionary.Add(keyId, 0);
}
KeyValueDictionary[keyId] += adjustment;
}
- Словарь пуст.
- Поток 1 входит в метод с
keyId = 17
. Поскольку словарь пуст, условное выражение в if
возвращает true
, а поток 1 достигает строки кода, помеченной A
.
- Поток 1 приостановлен, а поток 2 входит в метод с
keyId = 17
. Поскольку словарь пуст, условное выражение в if
возвращает true
, а поток 2 достигает строки кода, помеченной A
.
- Тема 2 приостановлена, а тема 1 возобновлена. Теперь поток 1 добавляет
(17, 0)
в словарь.
- Тема 1 приостановлена, и теперь тема 2 возобновляется. Теперь поток 2 пытается добавить
(17, 0)
в словарь. Возникло исключение из-за нарушения ключа.
Существуют другие сценарии, в которых может возникнуть исключение. Например, поток 1 может быть приостановлен при загрузке значения KeyValueDictionary[keyId]
(скажем, он загружает keyId = 17
и получает значение 42
), поток 2 может войти и изменить значение (скажем, он загружает keyId = 17
, добавляет корректировку 27
), и теперь поток 1 возобновляет и добавляет свою корректировку к загруженному значению (в частности, он не видит изменения, внесенного потоком 2 в значение, связанное с keyId = 17
!). *
Обратите внимание, что даже использование ConcurrentDictionary<TKey, TValue>
может привести к вышеперечисленным ошибкам! Ваш код НЕ является безопасным по причинам, не связанным с безопасностью потоков или отсутствием таковых для Dictionary<TKey, TValue>
.
Чтобы ваш код стал потокобезопасным с параллельным словарем, вам нужно будет сказать:
KeyValueDictionary.AddOrUpdate(keyId, adjustment, (key, value) => value + adjustment);
Здесь мы используем ConcurrentDictionary.AddOrUpdate
.