Блокировка не поточно-ориентированного объекта, это приемлемая практика? - PullRequest
4 голосов
/ 19 июля 2011

Я получил некоторую скорбь об этом в комментарии, который я опубликовал на днях, поэтому я хотел опубликовать вопрос в попытке, чтобы люди сказали мне, что я сумасшедший, что я приму, или скажите мне, что яможет быть и прав, что я тоже с удовольствием приму.Я также могу принять что-нибудь между ними.

Допустим, у вас есть необезопасный тип объекта, такой как Dictionary<int, string>.В качестве аргумента я знаю, что вы также можете использовать ConcurrentDictionary<int, string>, который является поточно-ориентированным, но я хочу поговорить об общих принципах, касающихся не поточно-ориентированных объектов в многопоточной среде.

Рассмотримследующий пример:

private static readonly Dictionary<int, string> SomeDictionary = new Dictionary<int, string>();
private static readonly object LockObj = new object();

public static string GetById(int id)
{
  string result;

  /** Lock Bypass **/
  if (SomeDictionary.TryGetValue(id, out result)
  {
    return result;
  }

  lock (LockObj)
  {
    if (SomeDictionary.TryGetValue(id, out result)
    {
      return result;
    }

    SomeDictionary.Add(id, result = GetSomeString());
  }

  return result;
}

Шаблон блокировки называется Блокировка с двойной проверкой , поскольку блокировка активно обходится, если словарь уже инициализирован с этим идентификатором.Метод «Добавить» словаря вызывается внутри блокировки, потому что мы хотим вызвать метод только один раз, потому что он вызовет исключение, если вы попытаетесь добавить элемент с тем же ключом.

Это был мойпонимание того, что этот шаблон блокировки по существу синхронизирует способ обработки словаря, что позволяет ему быть поточно-ориентированным.Но я получил несколько негативных комментариев о том, что это не делает его потокобезопасным.

Итак, мой вопрос: приемлем ли этот шаблон блокировки для не поточно-безопасных объектов в многопоточной среде?Если нет, то какой шаблон лучше использовать?(при условии, что нет идентичного типа C #, который является потокобезопасным)

Ответы [ 3 ]

8 голосов
/ 19 июля 2011

Нет, это не безопасно. Метод TryGetValue просто не является потокобезопасным, поэтому его не следует использовать, когда объект совместно используется несколькими потоками без блокировки. Шаблон блокировки с двойной проверкой включает в себя только тестирование ссылки - которая, хотя она не гарантирует получение обновленного результата, не вызовет других проблем. Сравните это с TryGetValue, который может делать что угодно (например, генерировать исключение, повреждать внутреннюю структуру данных), если вызывается одновременно, скажем, Add.

Лично я бы просто использовал блокировку, но вы могли бы потенциально использовать ReaderWriterLockSlim. (В большинстве случаев простая блокировка будет более эффективной - но это зависит от того, сколько времени занимают операции чтения и записи и на что похожи споры.)

2 голосов
/ 19 июля 2011

Это небезопасно, поскольку второй поток потенциально может прочитать значение из SomeDictionary, пока словарь находится в несогласованном состоянии.

Рассмотрим следующий сценарий:

  1. Поток A пытается получить идентификатор 3. Он не существует, поэтому он получает блокировку и вызывает Add, но прерывается на полпути через метод.
  2. Поток B пытается получить идентификатор 3. Вызов Add зашел достаточно далеко, чтобы метод вернул (или попытался вернуться) true.

Теперь может случиться множество плохих вещей. Возможно, что поток B увидит, что первый TryGetValue (вне блокировки) вернет true, но возвращаемое значение не имеет смысла, потому что реальное значение еще не было сохранено. Другая возможность состоит в том, что реализация Dictionary понимает, что находится в несогласованном состоянии, и выдает InvalidOperationException. Или это может не произойти, это может просто продолжаться с поврежденным внутренним состоянием. В любом случае, плохой моджо .

1 голос
/ 19 июля 2011

Просто удалите первый TryGetValue, и все будет в порядке.

/** Lock Bypass **/
if (SomeDictionary.TryGetValue(id, out result)
{
    return result;
}

Не используйте ReaderWriterLock или ReaderWriterLockSlim, если вы не выполняете менее 20% операций записи, а рабочая нагрузка в пределах блокировки достаточно значительна, чтобы параллельчитает будет иметь значение.В качестве примера, следующее демонстрирует, что простой оператор lock () превзойдет использование блокировок чтения / записи, когда операция чтения / записи проста.

internal class MutexOrRWLock
{
    private const int LIMIT = 1000000;
    private const int WRITE = 100;//write once every n reads

    private static void Main()
    {
        if (Environment.ProcessorCount < 8)
            throw new ApplicationException("You must have at least 8 cores.");
        Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(255); // pin the process to first 8 CPUs

        Console.WriteLine("ReaderWriterLock");
        new RWLockTest().Test(3);
        Console.WriteLine("ReaderWriterLockSlim");
        new RWSlimTest().Test(3);
        Console.WriteLine("Mutex");
        new MutexTest().Test(3);
    }

    private class RWLockTest : MutexTest
    {
        private readonly ReaderWriterLock _lock1 = new ReaderWriterLock();

        protected override void BeginRead() { _lock1.AcquireReaderLock(-1); }
        protected override void EndRead() { _lock1.ReleaseReaderLock(); }

        protected override void BeginWrite() { _lock1.AcquireWriterLock(-1); }
        protected override void EndWrite() { _lock1.ReleaseWriterLock(); }
    }

    private class RWSlimTest : MutexTest
    {
        private readonly ReaderWriterLockSlim _lock1 = new ReaderWriterLockSlim();

        protected override void BeginRead() { _lock1.EnterReadLock(); }
        protected override void EndRead() { _lock1.ExitReadLock(); }

        protected override void BeginWrite() { _lock1.EnterWriteLock(); }
        protected override void EndWrite() { _lock1.ExitWriteLock(); }
    }

    private class MutexTest
    {
        private readonly ManualResetEvent start = new ManualResetEvent(false);
        private readonly Dictionary<int, int> _data = new Dictionary<int, int>();

        public void Test(int count)
        {
            for (int i = 0; i < count; i++)
            {
                _data.Clear();
                for (int val = 0; val < LIMIT; val += 3)
                    _data[val] = val;

                    start.Reset();
                Thread[] threads = new Thread[8];
                for (int ti = 0; ti < 8; ti++)
                    (threads[ti] = new Thread(Work)).Start();

                Thread.Sleep(1000);
                Stopwatch sw = new Stopwatch();
                sw.Start();
                start.Set();
                foreach (Thread t in threads)
                    t.Join();
                sw.Stop();
                Console.WriteLine("Completed: {0}", sw.ElapsedMilliseconds);
            }
        }

        protected virtual void BeginRead() { Monitor.Enter(this); }
        protected virtual void EndRead() { Monitor.Exit(this); }

        protected virtual void BeginWrite() { Monitor.Enter(this); }
        protected virtual void EndWrite() { Monitor.Exit(this); }

        private void Work()
        {
            int val;
            Random r = new Random();
            start.WaitOne();
            for (int i = 0; i < LIMIT; i++)
            {
                if (i % WRITE == 0)
                {
                    BeginWrite();
                    _data[r.Next(LIMIT)] = i;
                    EndWrite();
                }
                else
                {
                    BeginRead();
                    _data.TryGetValue(i, out val);
                    EndRead();
                }
            }
        }
    }
}

Предыдущая программа выводит следующеерезультаты на моем ПК:

ReaderWriterLock
Completed: 2412
Completed: 2385
Completed: 2422

ReaderWriterLockSlim
Completed: 1374
Completed: 1397
Completed: 1491

Mutex
Completed: 763
Completed: 750
Completed: 758
...