Який найкращий спосіб впровадження безпечного для словника словника?


109

Мені вдалося реалізувати безпечний для потоків словник у C #, виходячи з IDictionary та визначивши приватний об’єкт SyncRoot:

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public object SyncRoot
    {
        get { return syncRoot; }
    } 

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
    }

    // more IDictionary members...
}

Потім я блокую цей об’єкт SyncRoot у всіх своїх споживачів (кілька потоків):

Приклад:

lock (m_MySharedDictionary.SyncRoot)
{
    m_MySharedDictionary.Add(...);
}

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


3
Що це ти потворно вважаєш?
Асаф Р

1
Я думаю, що він має на увазі всі заяви про блокування, які він має у своєму коді в межах споживачів класу SharedDictionary - він фіксує код виклику кожен раз, коли він отримує доступ до методу на об'єкті SharedDictionary.
Пітер Мейєр

Замість використання методу Add спробуйте зробити це, призначивши значення ex-m_MySharedDictionary ["key1"] = "item1", це безпечно для потоків.
тестувальник

Відповіді:


43

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

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
        OnItemAdded(EventArgs.Empty);
    }

    public event EventHandler ItemAdded;

    protected virtual void OnItemAdded(EventArgs e)
    {
        EventHandler handler = ItemAdded;
        if (handler != null)
            handler(this, e);
    }

    // more IDictionary members...
}

Редагувати: Документи MSDN зазначають, що перерахування по суті не є безпечним для потоків. Це може бути однією з причин експонування об’єкта синхронізації поза вашим класом. Інший спосіб підходу, який би полягав у наданні певних методів виконання дії над усіма членами та замикання навколо перерахунку членів. Проблема в цьому полягає в тому, що ви не знаєте, чи буде дія, передана цій функції, викликає якогось члена вашого словника (це призведе до тупикової ситуації). Розкриття об’єкта синхронізації дозволяє споживачеві приймати ці рішення і не приховує тупик всередині вашого класу.


@fryguybob: Перерахування насправді була єдиною причиною, чому я виставляв об’єкт синхронізації. За умовою, я б робив блокування на цьому об'єкті лише тоді, коли я перераховував їх через колекцію.
GP.

1
Якщо ваш словник не надто великий, ви можете перерахувати його на копії та мати вбудований у клас.
fryguybob

2
Мій словник не надто великий, і я думаю, що це зробив трюк. Я зробив новий публічний метод під назвою CopyForEnum (), який повертає новий екземпляр Словника з копіями приватного словника. Потім цей метод був викликаний для перерахунку, і SyncRoot було видалено. Дякую!
GP.

12
Це також не є власне безпечним класом, оскільки словникові операції мають тенденцію до деталізації. Трохи логіки за принципом if (dict.Contains (what)) {dict.Remove (що завгодно); dict.Add (що завгодно, newval); }, безумовно, умова гонки, яка чекає, щоб відбутися
плінтус

207

Названий клас .NET 4.0, який підтримує паралельність ConcurrentDictionary.


4
Будь ласка, позначте це як відповідь, вам не потрібен користувальницький довідник, якщо власний. У мережі є рішення
Альберто Леон

27
(Пам'ятайте, що інша відповідь була написана задовго до існування .NET 4.0 (випущена в 2010 р.).
Jeppe Stig Nielsen

1
На жаль, це не безблокове рішення, тому марно в безпечних збірках SQL Server CLR. Вам знадобиться щось на зразок описаного тут: cse.chalmers.se/~tsigas/papers/Lock-Free_Dictionary.pdf або, можливо, ця реалізація: github.com/hackcraft/Ariadne
Triynko

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

60

Спроба внутрішньої синхронізації майже напевно буде недостатньою, оскільки це занадто низький рівень абстракції. Скажіть, ви робитеAdd та ContainsKeyвиконуючи операції в індивідуальному порядку, таким чином:

public void Add(TKey key, TValue value)
{
    lock (this.syncRoot)
    {
        this.innerDictionary.Add(key, value);
    }
}

public bool ContainsKey(TKey key)
{
    lock (this.syncRoot)
    {
        return this.innerDictionary.ContainsKey(key);
    }
}

Тоді що відбувається, коли ви називаєте цей нібито безпечний потік коду з декількох потоків? Чи завжди це буде добре?

if (!mySafeDictionary.ContainsKey(someKey))
{
    mySafeDictionary.Add(someKey, someValue);
}

Проста відповідь - ні. У якийсь моментAdd метод видасть виняток, який вказує на те, що ключ вже є у словнику. Як це можна зробити з безпечним для потоків словником, запитаєте ви? Отже, оскільки кожна операція є безпечною для потоків, поєднання двох операцій не є, оскільки інший потік може змінити її між вашим викликом до ContainsKeyта Add.

Що означає правильно написати цей сценарій, вам потрібен замок зовні словником, наприклад

lock (mySafeDictionary)
{
    if (!mySafeDictionary.ContainsKey(someKey))
    {
        mySafeDictionary.Add(someKey, someValue);
    }
}

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

  1. Використовуйте звичайне Dictionary<TKey, TValue> та синхронізуйте зовнішньо, додаючи до неї складові операції, або

  2. Напишіть нову захищену від потоку обгортку з іншим інтерфейсом (тобто не IDictionary<T>), який поєднує такі операції, як anAddIfNotContained метод, щоб ніколи не потрібно комбінувати операції з неї.

(Я схильний ходити з №1 сам)


10
Варто зазначити, що .NET 4.0 буде включати цілу купу безпечних для потоків контейнерів, таких як колекції та словники, які мають інший інтерфейс до стандартної колекції (тобто вони роблять варіант 2 для вас вище).
Грег Бук

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

6

Ви не повинні публікувати приватний об'єкт блокування через ресурс. Об'єкт блокування повинен існувати приватно з єдиною метою виступати в ролі пункту рандеву.

Якщо продуктивність виявиться низькою за допомогою стандартного блокування, то колекція замків Wintellect Power Threading може бути дуже корисною.


5

Існує кілька проблем із описаним вами способом реалізації.

  1. Ніколи не слід виставляти об’єкт синхронізації. Це дозволить споживачеві захопити предмет і зняти замок на ньому, і тоді ви будете тости.
  2. Ви реалізуєте безпечний інтерфейс без потоку з безпечним класом для потоку. ІМХО, це коштуватиме вам вниз

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


3

Вам не потрібно блокувати властивість SyncRoot у споживчих об'єктах. Блокування, яке ви маєте в методах словника, є достатнім.

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

У вашому випадку відбувається наступне:

Say нитка А набуває замок на SyncRoot перед викликом до m_mySharedDictionary.Add. Потім нитка B намагається придбати замок, але блокується. Насправді всі інші потоки заблоковані. Нитка А дозволена зайти в метод Додати. У операторі блокування в рамках методу Add, потоку A дозволено отримати замок знову, оскільки він вже є ним. Після виходу з контексту блокування в методі, а потім поза методом, потік A випустив усі блоки, що дозволяють продовжувати інші потоки.

Ви можете просто дозволити будь-якому споживачеві звертатися до методу Add, оскільки операція блокування у вашому методі Add SharedDictionary матиме такий же ефект. У цей момент у вас є зайве блокування. Ви б заблокували SyncRoot за межами одного із методів словника, якщо вам довелося б виконати дві операції над об’єктом словника, які потрібно гарантувати, що вони відбуватимуться послідовно.


1
Неправда ... якщо ви робите дві операції за іншою, які є внутрішньо потоковими, це не означає, що загальний блок коду є безпечним для потоків. Наприклад: if (! MyDict.ContainsKey (someKey)) {myDict.Add (someKey, someValue); } не буде безпечним для потоків, навіть це ContainsKey і Add - це безпечні теми
Тим

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

Я думаю, якщо все, що він коли-небудь робить, - це додавання до словника, але оскільки у нього є "// більше членів IDictionary ...", я припускаю, що в якийсь момент він також захоче прочитати дані зі словника. Якщо це так, то повинен бути якийсь зовнішньо доступний механізм блокування. Не має значення, чи це SyncRoot у самому словнику чи інший об’єкт, який використовується виключно для блокування, але без такої схеми загальний код не буде безпечним для потоків.
Тім

Зовнішній механізм блокування є таким, як він показує у своєму прикладі у питанні: lock (m_MySharedDictionary.SyncRoot) {m_MySharedDictionary.Add (...); } - було б абсолютно безпечно зробити наступне: lock (m_MySharedDictionary.SyncRoot) {if (! m_MySharedDictionary.Contains (...)) {m_MySharedDictionary.Add (...); }} Іншими словами, зовнішній механізм блокування - це твердження про блокування, яке працює на загальнодоступній власності SyncRoot.
Пітер Мейєр

0

Просто думка, чому б не відтворити словник? Якщо читання - це безліч записів, то блокування синхронізує всі запити.

приклад

    private static readonly object Lock = new object();
    private static Dictionary<string, string> _dict = new Dictionary<string, string>();

    private string Fetch(string key)
    {
        lock (Lock)
        {
            string returnValue;
            if (_dict.TryGetValue(key, out returnValue))
                return returnValue;

            returnValue = "find the new value";
            _dict = new Dictionary<string, string>(_dict) { { key, returnValue } };

            return returnValue;
        }
    }

    public string GetValue(key)
    {
        string returnValue;

        return _dict.TryGetValue(key, out returnValue)? returnValue : Fetch(key);
    }

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