.NET - Блокування словника проти ConcurrentDictionary


131

Я не міг знайти достатньо інформації про ConcurrentDictionaryтипи, тому думав, що запитаю про це тут.

В даний час я використовую a Dictionaryдля утримання всіх користувачів, до яких постійно звертаються декілька потоків (з пулу потоків, тому немає точної кількості потоків), і він має синхронізований доступ.

Нещодавно я дізнався, що в .NET 4.0 був набір колекцій, захищених від потоків, і це, здається, дуже приємно. Мені було цікаво, що було б "ефективнішим та простішим у керуванні" варіантом, оскільки у мене є варіант між нормальним Dictionaryіз синхронізованим доступом або тим, ConcurrentDictionaryщо вже є безпечним для потоків.

Посилання на .NET 4.0 ConcurrentDictionary


3
Можливо, ви хочете побачити цю статтю з кодовим проектом на цю тему
nawfal

Відповіді:


146

Набір колекцій, захищених від ниток, порівняно з колекцією, що не є безпечною для потоків, може розглядатися по-різному.

Розгляньте магазин без канцелярії, за винятком каси. У вас є маса проблем, якщо люди не діють відповідально. Наприклад, скажімо, що клієнт бере пилочку з піраміди, тоді як клерк будує піраміду, все пекло зірветься. Або що робити, якщо двоє клієнтів одночасно досягають одного і того ж товару, хто виграє? Чи буде бійка? Це колекція, яка не є безпечною. Існує маса способів уникнути проблем, але всі вони потребують певного блокування, а точніше явного доступу в той чи інший спосіб.

З іншого боку, розгляньте магазин з канцеляристою за столом, і ви можете робити покупки лише через нього. Ви переходите до черги і просите у нього предмет, він повертає його вам, а ви виходите з лінії. Якщо вам потрібно кілька предметів, ви можете забрати стільки предметів, котрі ви можете згадати, але вам потрібно бути обережними, щоб уникнути зависання діловодом, це злість інших клієнтів у черзі за вами.

Тепер розглянемо це. У магазині з одним канцеляристом, що робити, якщо ви добираєтесь до передньої частини рядка, і запитайте у ділової особи "Чи є у вас туалетний папір", і він каже "Так", а потім ви йдете "Добре, я" Я повернуся до вас, коли я знаю, скільки мені потрібно ", тоді до моменту, коли ви знову на передній лінії, магазин, звичайно, може бути розпроданий. Цей сценарій не перешкоджає колекції безпечних ниток.

Колекція безпечних потоків гарантує, що її внутрішні структури даних є дійсними в усі часи, навіть якщо доступ до них здійснюється з декількох потоків.

Колекція, що не містить безпеку, не має таких гарантій. Наприклад, якщо ви додасте щось до бінарного дерева на одній нитці, а інша нитка зайнята відновленням балансу дерева, немає жодної гарантії, що елемент буде доданий, або навіть те, що дерево все-таки дійсне згодом, воно може бути зіпсованим поза надією.

Однак колекція безпечних потоків не гарантує, що послідовні операції над потоком працюють над тим самим "знімком" його внутрішньої структури даних, що означає, що якщо у вас є такий код:

if (tree.Count > 0)
    Debug.WriteLine(tree.First().ToString());

ви можете отримати NullReferenceException, оскільки між tree.Countі tree.First()інша нитка очистила решта вузлів у дереві, що означає First(), що повернеться null.

Для цього сценарію вам або потрібно перевірити, чи є у відповідної колекції безпечний спосіб отримати те, що ви хочете, можливо, вам потрібно буде переписати код вище, або, можливо, вам доведеться заблокувати.


9
Кожен раз, коли я читаю цю статтю, хоча це все правда, ми якось йдемо неправильним шляхом.
obseg_creep

19
Основна проблема в програмі з підтримкою потоку - це змінні структури даних. Якщо ви зможете цього уникнути, ви врятуєте собі безліч неприємностей.
Лассе В. Карлсен

89
Це приємне пояснення безпеки потоку, проте я не можу не відчути, що це насправді не стосується питання ОП. Те, що запитував ОП (і що я згодом натрапив на це питання, шукаючи), було різницею між використанням стандарту Dictionaryта керуванням блокуванням себе проти використанням ConcurrentDictionaryтипу, вбудованого в .NET 4+. Я насправді трохи збентежений тим, що це було прийнято.
mclark1129

4
Безпека ниток - це більше, ніж просто використання правильної колекції. Використання правильної колекції - це початок, не єдине, з чим вам доведеться мати справу. Я думаю, це те, що хотіла знати ОП. Я не можу здогадатися, чому це було прийнято, минув час, коли я це написав.
Лассе В. Карлсен

2
Я думаю, що @ LasseV.Karlsen намагається сказати, це те, що ... безпеку ниток досягти легко, але ви повинні забезпечити рівень одночасності в тому, як ви поводитеся з цією безпекою. Запитайте себе так: "Чи можу я робити більше операцій одночасно, зберігаючи об'єктну нитку безпечно?" Розглянемо цей приклад: два клієнти хочуть повернути різні предмети. Коли ви, як менеджер, здійснюєте поїздку, щоб відновити свій магазин, чи можете ви не просто перезавантажувати обидва товари за одну поїздку і все ще обробляти запити обох клієнтів, або збираєтесь здійснити поїздку щоразу, коли клієнт поверне товар?
Олександру

68

Ви все ще повинні бути дуже обережними при використанні колекцій, захищених від потоку, тому що безпека потоків не означає, що ви можете ігнорувати всі проблеми з ниткою. Коли колекція рекламує себе як безпечна для потоків, це зазвичай означає, що вона залишається в послідовному стані, навіть коли багато потоків читають і записують одночасно. Але це не означає, що один потік побачить "логічну" послідовність результатів, якщо він викликає кілька методів.

Наприклад, якщо спочатку перевірити, чи існує ключ, а потім отримати значення, яке відповідає ключу, цей ключ може більше не існувати навіть у версії ConcurrentDictionary (оскільки інший потік міг видалити ключ). У цьому випадку вам все одно потрібно застосувати блокування (або краще: поєднати два виклики за допомогою TryGetValue ).

Тож використовуйте їх, але не думайте, що це дає вам безкоштовний пропуск, щоб ігнорувати всі проблеми одночасності. Ще потрібно бути обережним.


4
Отже, ви в основному говорите, якщо ви неправильно не використовуєте об'єкт, він не працює?
ChaosPandion

3
В основному вам потрібно використовувати TryAdd замість загальної Містить -> Додати.
ChaosPandion

3
@Bruno: Ні. Якщо ви не заблокували, інший потік, можливо, видалив ключ між двома дзвінками.
Марк Байєрс

13
Не маю на увазі звучати криво, але TryGetValueце завжди було частиною Dictionaryі завжди було рекомендованим підходом. Важливими "паралельними" методами, які ConcurrentDictionaryвпроваджуються, є AddOrUpdateі GetOrAdd. Отже, хороша відповідь, але міг обрати кращий приклад.
Aaronaught

4
Право - на відміну від бази даних, яка має транзакції, більшість паралельних структур пошуку в пам'яті пропускають концепцію ізоляції , вони гарантують лише правильність внутрішньої структури даних.
Чанг

39

Внутрішньо ConcurrentDictionary використовує окремий замок для кожного хеш-відра. Поки ви використовуєте лише Add / TryGetValue і подібні методи, які працюють над окремими записами, словник буде працювати як майже безблокова структура даних із відповідною перевагою роботи. OTOH методи перерахування (включаючи властивість Count) блокують усі відра одразу і, отже, гірші за синхронізований словник.

Я б сказав, просто використовуйте ConcurrentDictionary.


15

Я думаю, що метод ConcurrentDictionary.GetOrAdd саме те, що потрібно більшості багатопотокових сценаріїв.


1
Я б також використав TryGet, інакше ви витрачаєте зусилля на ініціалізацію другого параметра в GetOrAdd кожного разу, коли ключ вже існує.
Хорріт Шипперс

7
У GetOrAdd є перевантаження GetOrAdd (TKey, Func <TKey, TValue>) , тому другий параметр може бути ініціалізований лінією лише у випадку, якщо ключ не є у словнику. Крім того, TryGetValue використовується для отримання даних, не змінюючи їх. Подальші дзвінки до кожного з цих способів можуть призвести до того, що інший потік, можливо, додав або видалив ключ між двома викликами.
sgnsajgon

Це має бути помітною відповіддю на питання, навіть незважаючи на те, що посада Лассе чудова.
Spivonious

13

Чи бачили Ви реактивні розширення для .Net 3.5sp1. За словами Джона Скіта, вони підтримали пакет паралельних розширень та паралельних структур даних для .Net3.5 sp1.

Існує набір зразків для .Net 4 Beta 2, який досить добре описує, як використовувати їх паралельні розширення.

Я щойно провів останній тиждень, тестуючи ConcurrentDictionary, використовуючи 32 потоки для виконання вводу-виводу. Здається, він працює як рекламований, що свідчить про те, що в нього було вкладено величезну кількість тестувань.

Редагувати : .NET 4 ConcurrentDictionary та шаблони.

Microsoft випустила pdf під назвою Patterns of Paralell програмування. Насамперед варто завантажити, як це описано в дуже приємних подробицях правильних моделей, які слід використовувати для. Net 4 Паралельних розширень та анти-шаблонів, яких слід уникати. Ось.


4

В основному ви хочете піти з новим ConcurrentDictionary. Безпосередньо з поля ви повинні написати менше коду, щоб зробити безпечні програми для потоків.


Чи є якесь питання, якщо я продовжую використовувати одночасні диктейнери?
кбвішну

1

The ConcurrentDictionary чудовий варіант, якщо він відповідає всім вашим потребам безпеки. Якщо це не так, іншими словами, ви робите щось трохи складне, нормальний Dictionary+ lockможе бути кращим варіантом. Наприклад, скажімо, що ви додаєте деякі замовлення до словника, і ви хочете постійно оновлювати загальну суму замовлень. Ви можете написати такий код:

private ConcurrentDictionary<int, Order> _dict;
private Decimal _totalAmount = 0;

while (await enumerator.MoveNextAsync())
{
    Order order = enumerator.Current;
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

Цей код не є безпечним для потоків. Кілька ниток, що оновлюють _totalAmountполе, можуть залишити його у зіпсованому стані. Тож ви можете спробувати захистити це за допомогою lock:

_dict.TryAdd(order.Id, order);
lock (_locker) _totalAmount += order.Amount;

Цей код "безпечніший", але все ще не є безпечним для потоків. Немає гарантії _totalAmountвідповідності записів у словнику. Інший потік може спробувати прочитати ці значення для оновлення елемента інтерфейсу:

Decimal totalAmount;
lock (_locker) totalAmount = _totalAmount;
UpdateUI(_dict.Count, totalAmount);

The totalAmountНе може відповідати кол - ву замовлень в словнику. Відображена статистика може бути неправильною. У цей момент ви зрозумієте, що ви повинні розширити lockзахист, щоб включити оновлення словника:

// Thread A
lock (_locker)
{
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

// Thread B
int ordersCount;
Decimal totalAmount;
lock (_locker)
{
    ordersCount = _dict.Count;
    totalAmount = _totalAmount;
}
UpdateUI(ordersCount, totalAmount);

Цей код є абсолютно безпечним, але всі переваги від використання a ConcurrentDictionaryвже відсутні.

  1. Продуктивність стала гіршою, ніж використання нормальної Dictionary , тому що внутрішня блокування всередині в ConcurrentDictionaryданий час є марною і зайвою.
  2. Ви повинні переглянути весь код на предмет незахищеного використання спільних змінних.
  3. Ви застрягли в використанні незручного API ( TryAdd?,AddOrUpdate ?).

Тому моя порада: почніть з позначки " Dictionary+" lockта збережіть опцію оновлення пізніше до рівня ConcurrentDictionaryоптимізації продуктивності, якщо цей варіант справді життєздатний. У багатьох випадках цього не буде.


0

Ми використовували ConcurrentDictionary для колекціонування кешу, який повторно заповнюється кожні 1 годину, а потім читається декількома потоками клієнтів, як рішення для цього потоку "Приклад безпечний?"питання.

Ми виявили, що зміна його на  ReadOnlyDictionary  покращила загальну ефективність.


ReadOnlyDictionary - це лише обгортка навколо іншого IDictionary. Що ви використовуєте під кришкою?
Lu55

@ Lu55, ми використовували бібліотеку MS .Net і вибирали серед доступних / застосовних словників. Мені
невідомо
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.