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

Вид: 20809|Відповідь: 1

[Джерело] ConcurrentDictionary vs. Dictionary+Locking - Денніс Гао

[Копіювати посилання]
Опубліковано 13.09.2016 13:33:04 | | |
До виходу .NET 4.0, якщо нам потрібно було використовувати клас Dictionary у багатопотоковому середовищі, ми не мали іншого вибору, окрім як самостійно реалізувати синхронізацію потоків, щоб зберегти їх у безпеці.

Багато розробників, безумовно, реалізували подібне рішення для безпеки потоків, або створивши абсолютно новий тип словника для потоків, або просто інкапсулюючи об'єкт словника в клас і додаючи механізм блокування до всіх методів, який ми називаємо «Словник + Блокування».

Але тепер у нас є ConcurrentDictionary. Опис Thread-safe у документації Dictionary class документації на MSDN стверджує, що якщо вам потрібно використовувати потрібно-безпечну реалізацію, використовуйте ConcurrentDictionary.

Отже, тепер, коли у нас є курс словника, безпечний для потоків, нам більше не потрібно впроваджувати його самостійно. Чудово, чи не так?

Походження проблеми

Насправді, я використовував CocurrentDictionary лише один раз у тесті, щоб перевірити його чутливість. Оскільки він добре пройшов тести, я одразу замінив його на свій клас, провів деякі тести, а потім щось пішло не так.

То що ж пішло не так? Хіба ти не казав, що нитка безпечна?

Після додаткових тестів я знайшов корінь проблеми. Але з якоїсь причини MSDN версія 4.0 не містить опису підпису методу GetOrAdd, який вимагає передачі параметра типу делегата. Після перегляду версії 4.5 я знайшов таку примітку:

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

Ці дані створюються дуже повільно;
Ці дані можна створити лише один раз, оскільки друге створення створює виняток, або кілька творів можуть призвести до витоку ресурсів тощо;
У мене була проблема з другим станом. Якщо обидва потоки виявляють, що певний фрагмент даних не існує, він буде створений один раз, але лише один результат буде успішно збережений. А як щодо іншої?

Якщо створений вами процес викликає виняток, ви можете скористатися try: Ловити (не досить елегантно, але вирішує проблему). Але що, якщо ресурс створюється, а не переробляється?

Можна сказати, що об'єкт створюється і буде збиратися як сміття, якщо на нього більше не посилається. Однак розглянемо, що сталося б, якби виникла описана нижче ситуація:

Генеруйте код динамічно за допомогою Emit. Я використав цей підхід у віддаленому фреймворку і помістив усі реалізації в асемблер, який не можна було переробити. Якщо тип створюється двічі, другий завжди існуватиме, навіть якщо він ніколи не використовувався.
Створіть тему прямо або опосередковано. Наприклад, нам потрібно створити компонент, який використовує власний потік для обробки асинхронних повідомлень і залежить від порядку їх отримання. Коли компонент створюється, створюється потік. Коли цей екземпляр компонента знищується, потік також завершується. Але якщо ми видаляємо посилання на об'єкт після знищення компонента, потік з якоїсь причини не закінчується і зберігає посилання на об'єкт. Тоді, якщо нитка не зникає, об'єкт також не буде перероблений.
Виконайте операцію P/Invoke. Вимагайте, щоб кількість часу закриття для отриманої ручки була такою ж, як кількість відкриттів.
Звісно, є багато схожих ситуацій. Наприклад, об'єкт словника утримує з'єднання з сервісом на віддаленому сервері, яке можна запросити лише один раз, і якщо запитати вдруге, інший сервіс подумає, що сталася якась помилка, і внесе його в журнал. (У компанії, де я працював, існували юридичні штрафи за цей стан.) )
Тож легко побачити, що Dictionary + Locks не можна швидко замінити на ConcurrentDictionary, навіть якщо в документації зазначено, що це безпечно для потоків.

Проаналізуйте проблему

Досі не розумієш?

Правда, що ця проблема може не виникати за підходом Словник + Замки. Оскільки це залежить від конкретної реалізації, розглянемо цей простий приклад:


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

Це добре, чи не так?

Насправді ні! Я не думаю, що створення об'єкта паралельно, де в кінці використовується лише один, не створює описану мною проблему.

Ситуація і проблема, яку я намагаюся розкрити, не завжди можна відтворити, у паралельному середовищі ми можемо просто створити два об'єкти і потім відкинути один. Отже, як саме порівняти Dictionary + Locks і ConcurrentDictionary?

Відповідь така: це залежить від стратегії використання замків і того, як використовується словник.

Гра 1: Створити один і той самий об'єкт паралельно

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

По-друге, скільки часу ми витрачаємо на подібні творіння?

Ми можемо просто побудувати приклад, де інстанція об'єкта займає 10 секунд. Коли перший потік створює об'єкт через 5 секунд, друга реалізація намагається викликати метод GetOrAdd для отримання об'єкта, і оскільки об'єкт досі не існує, вона також починає створювати об'єкт.

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

Якщо другий потік просто чекає, а другий процесор виконує іншу роботу (запускає інші потоки або додатки, економить енергію), він отримає потрібний об'єкт через 5 секунд замість 10.

Отже, за таких умов Dictionary + Locks виграє невелику гру.

Гра 2: Відвідування різних об'єктів паралельно

Ні, ситуація, про яку ви сказали, зовсім не є правдою!

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

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

Таким чином, ConcurrentDictionary відмовився від гри.

Примітка: Тут я вважаю, що ви розумієте кілька понять, таких як Bucket/Node/Entry, у словниковому курсі; якщо ні — рекомендується прочитати статтю Офіра Макмала «Глибоке розуміння загального словника», яка добре пояснює ці поняття.

Третя гра гри: читати далі і писати одиночну

Що станеться, якщо ви використовуєте Multiple Readers і Single Writer замість повного блокування словника в Dictionary + Locks?

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

Ми також можемо вирішити проблему, залишивши операцію зчитування в режимі простою на 10 секунд. Але якщо читання значно більше, ніж записів, ми побачимо, що ConcurrentDictionary все одно швидкий, бо реалізує режим читання без блокування.

Використання ReaderWriterLockSlim для словників погіршує читання, тому зазвичай рекомендується використовувати Full Lock для словників замість ReaderWriterLockSlim.

Отже, за таких умов ConcurrentDictionary виграв ще одну гру.

Примітка: я вже розглядав курси YieldReaderWriterLock та YieldReaderWriterLockSlim у попередніх статтях. Завдяки цьому блокуванню читання-запису швидкість значно покращилася (тепер еволюція у SpinReaderWriterLockSlim) і дозволяє виконувати кілька читань паралельно з мінімальним або відсутнім впливом. Хоч я й досі використовую цей спосіб, беззамковий Concurrent Dictionary, очевидно, буде швидшим.

Гра 4: Додайте кілька пар ключ-значення

Протистояння ще не закінчилося.

Що, якщо у нас є кілька значень ключів для додавання, і всі вони не стикаються і призначаються в різних категоріях?

Спочатку це питання було цікавим, але я провів тест, який не зовсім підходив. Я використав словник типу <int, int> і фабрика будівництва об'єкта безпосередньо повертала негативний результат як ключ.

Я очікував, що ConcurrentDictionary буде найшвидшим, але він виявився найповільнішим. Dictionary + Locks, навпаки, працює швидше. Чому так?

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

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

Отже, Словник + Замки виграють цю гру.

Граючи у п'яту гру: Частота операцій зчитування вища

Чесно кажучи, якби у нас був делегат, який міг би швидко створювати об'єкти, нам не потрібен був би словник. Ми можемо безпосередньо викликати делегата, щоб отримати об'єкт, так?

Насправді відповідь також залежить від ситуації.

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

Створення такого об'єкта відбувається майже миттєво. Після цього не потрібно створювати новий об'єкт, просто змінюйте збережені в ньому значення. Отже, можна дозволити створення способу двічі, доки не буде використано лише один екземпляр. Однак, оскільки ConcurrentDictionary розподіляє ресурси вузлів повільніше, використання Dictionary + Locks призведе до швидшого створення.

Отже, цей приклад дуже особливий, ми також бачимо, що Dictionary + Locks працює краще за таких умов, займаючи менше часу.

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

Але в більшості випадків, як тільки елемент даних створено, його завжди читають. Як змінюється вміст елемента даних — це інше питання. Тож не має значення, скільки мілісекунд займе створення об'єкта даних, адже читання відбувається швидше (всього на кілька мілісекунд), але читання відбувається частіше.

Отже, ConcurrentDictionary виграв гру.

Гра 6: Створіть об'єкти, які займають різний час

Що станеться, якщо час, необхідний для створення різних елементів даних, відрізняється?

Створіть кілька елементів даних, які займають різний час, і додайте їх до словника паралельно. Це найсильніша сторона ConcurrentDictionary.

ConcurrentDictionary використовує низку різних механізмів блокування, щоб одночасно додавати елементи даних, але логіка, така як вибір блокування, запит на блокування для зміни розміру відра тощо, не допомагає. Швидкість, з якою елементи даних потрапляють у відро, дуже швидка. Те, що справді робить ConcurrentDictionary переможцем, — це здатність створювати об'єкти паралельно.

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

* Зверніть увагу, що я використовую словник типу <int, int>.

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

висновок

Отже, чи є висновок?

На даний момент ще є деякі:

Усі курси словника дуже швидкі. Хоча я створив мільйони даних, це все одно швидко. Зазвичай ми створюємо лише невелику кількість елементів даних, і між читаннями є певні часові інтервали, тому ми зазвичай не помічаємо витрат на читання елементів даних.
Якщо один і той самий об'єкт не можна створити двічі, не використовуйте ConcurrentDictionary.
Якщо вас дуже турбує продуктивність, Dictionary + Locks може бути хорошим рішенням. Важливим фактором є кількість доданих і видалених елементів даних. Але якщо є багато операцій читання, це повільніше, ніж ConcurrentDictionary.
Хоча я не вводив її, насправді існує більше свободи для використання схеми Словник + Замки. Наприклад, ви можете заблокувати один раз, додати кілька елементів даних, видалити кілька елементів даних або зробити запит кілька разів тощо, а потім зняти блокування.
Загалом, уникайте використання ReaderWriterLockSlim, якщо читання значно більше, ніж записів. Типи словників вже набагато швидші, ніж блокування читання у режимі читання-запису. Звісно, це також залежить від часу, витраченого на створення об'єкта в замку.
Тож, на мою думку, наведені приклади трохи надмірні, але вони показують, що використання ConcurrentDictionary не завжди є найкращим рішенням.

Відчуй різницю

Я написав цю статтю з наміром знайти краще рішення.

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

Можна стверджувати, що Bucket і Node у ConcurrentDictionary дуже прості. Я робив щось подібне, коли намагався створити курс словника. Звичайний курс словника може здаватися простішим, але насправді він є складнішим.

У ConcurrentDictionary кожен вузол є повним класом. У класі Dictionary Node реалізується за допомогою типу значення, і всі вузли зберігаються у величезному масиві, тоді як Bucket використовується для індексації масиву. Він також використовується замість простого посилання Вузла на його наступний Вузол (адже, як Вузол типу структури, він не може містити член Вузла типу структури).

При додаванні та видаленні словника клас Словник не може просто створити новий вузол, він повинен перевірити, чи є індекс, що позначає вузол, який був видалений, і потім повторно його використовувати. Або «Count» використовується для визначення позиції нового вузла в масиві. Насправді, коли масив заповнений, клас Словника змушує змінювати розмір.

Для ConcurrentDictionary Node можна розглядати як новий об'єкт. Видалення вузла — це просто видалення його посилання. Додавання нового вузла може просто створити новий екземпляр вузла. Зміна розміру лише для уникнення конфліктів, але не є обов'язковою.

Отже, якщо клас Dictionary навмисно використовує складніші алгоритми для обробки цього процесу, як ConcurrentDictionary забезпечить кращу роботу в багатопотоковому середовищі?

Правда в тому, що розміщення всіх вузлів в одному масиві — найшвидший спосіб розподілити та прочитати, навіть якщо нам потрібен інший масив для відстеження, де знаходити ці елементи. Отже, схоже, що однакова кількість бакет займе більше пам'яті, але нові елементи даних не потрібно перерозподіляти, не потрібні нові синхронізації об'єктів, і новий збір сміття не відбувається. Бо все вже готово.

Однак заміна вмісту у вузлі не є атомарною операцією, що є одним із факторів, що робить його потік небезпечним. Оскільки вузли — це всі об'єкти, спочатку створюється вузол, а потім оновлюється окреме посилання, щоб вказати на нього (атомарна операція тут). Отже, тред читання може читати зміст словника без блокування, і читання має бути одним із старих і нових значень, і немає шансу прочитати неповне значення.

Отже, правда така: якщо вам не потрібен блокування, курс Словника швидший на читання, бо саме блокування уповільнює читання.

Ця стаття перекладена зі статті Пауло Земека «Dictionary + Locking versus ConcurrentDictionary» на CodeProject, і деякі твердження зміняться з міркувань розуміння.







Попередній:IC-ефективний Autofac
Наступний:Alibaba 4 людини були звільнені за використання JS-скриптів, щоб поспішно купувати місячні кекси
 Орендодавець| Опубліковано 13.09.2016 13:33:15 |
ConcurrentDictionary підтримує нові та оновлені оновлення
http://www.itsvse.com/thread-2955-1-1.html
(Джерело: Мережа сільського господарства кодів)
Застереження:
Усе програмне забезпечення, програмні матеріали або статті, опубліковані Code Farmer Network, призначені лише для навчання та досліджень; Вищезазначений контент не повинен використовуватися в комерційних чи незаконних цілях, інакше користувачі несуть усі наслідки. Інформація на цьому сайті надходить з Інтернету, і спори щодо авторських прав не мають до цього сайту. Ви повинні повністю видалити вищезазначений контент зі свого комп'ютера протягом 24 годин після завантаження. Якщо вам подобається програма, будь ласка, підтримуйте справжнє програмне забезпечення, купуйте реєстрацію та отримайте кращі справжні послуги. Якщо є будь-яке порушення, будь ласка, зв'яжіться з нами електронною поштою.

Mail To:help@itsvse.com