Особенности блокировки для заполнения ConcurrentDictionary значениями базы данных в C # - PullRequest
3 голосов
/ 23 марта 2012

Нужно ли использовать блок lock (lockObj) {}, когда я заполняю здесь свой ConcurrentDictionary?Для небольшого фона это будет использоваться в приложении MVC, хотя я подозреваю, что вопрос о сценарии актуален для любого многопоточного приложения.

При поиске в стеке потока я не нашел этого точного сценария.Когда в первый раз запрашивается значение из значения GetOptionById, оно может вызываться двумя отдельными потоками.

1) Было бы целесообразно сделать объекты списка частным статическим значением, которое вы блокируетевокруг в надежде не вызывать базу данных несколько раз, прежде чем заполнить ConcurrentDictionary?

2) Это (# 1 выше) вообще необходимо, или ConcurrentDictionary достаточно умен, чтобы решить это самостоятельно?Заранее спасибо за любой вклад.

public class MyOptions
{
    static string GetOptionById(int id)
    {
        if (options == null || options.Count <= 0)
            FillOptionList();
        return options[id];
    }

    static void FillOptionList()
    {
        List<MyBusinessObject> objects = DataAccessLayer.GetList();
        foreach (MyBusinessObject obj in objects)
            options.TryAdd(obj.Id, obj.Name);
    }

    private static ConcurrentDictionary<int, string> options = new ConcurrentDictionary<int, string>();
}

РЕДАКТИРОВАТЬ: Спасибо всем за ваш вклад, это будет более безопасный подход?

    public static string OptionById(int id)
    {
        if (!options.ContainsKey(id))
        {
            //perhaps this is a new option and we need to reload the list
            FillOptionsOrReturn(true /*force the fill*/);
            return (!options.ContainsKey(id)) ? "Option not found" : options[id];
        }
        else
            return options[id];
    }

    private static void FillOptionsOrReturn(bool forceFill = false)
    {
        List<MyBusinessClass> objectsFromDb = null;
        lock (lockObj)
        {
            if (forceFill || options == null || options.Keys.Count <= 0)
                reasons = DataAccessLayer.GetList();
        }
        if (objectsFromDb != null)
        {
            foreach (MyBusinessClass myObj in objectsFromDb)
                options.TryAdd(myObj.id, myObj.name);
        }
    }

    private static ConcurrentDictionary<int, string> options = new ConcurrentDictionary<int, string>();
    private static object lockObj = new object();

Ответы [ 4 ]

3 голосов
/ 23 марта 2012

То, что у тебя есть, определенно не безопасно. Рассмотрим:

Потоки X и Y оба вызывают GetOptionById примерно в одно и то же время. X замечает, что нужно заполнить словарь, и начинает это делать. Первый результат возвращается и добавляется в словарь.

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

Это выглядит как хороший кандидат для использования Lazy<T> ... вы можете выбрать подходящие опции там, так что только один поток может заполнять словарь за раз - второй поток будет ждать, пока первый не закончил, прежде чем продолжить , Таким образом, «заполнить словарь» фактически становится атомарным.

Если вам не нужно обновлять словарь после первой загрузки, вам может даже подойти просто Lazy<Dictionary<string, string>> - безопасно иметь несколько читателей, если нет писателей. Я верю , что Lazy<T> будет правильно обрабатывать барьеры памяти.

2 голосов
/ 23 марта 2012

Следующие проблемы могут возникнуть в вашем коде как есть.Если они приемлемы, то с вами все в порядке, а если нет, то вам нужно будет использовать блокировки.

  1. Несколько потоков могут понять, что options равно нулю, и заново создатьтолковый словарь.Это приведет к тому, что он будет заполнен несколько раз.
  2. Поток может читать из словаря, хотя некоторые, но не все элементы были добавлены.
1 голос
/ 23 марта 2012

Нужно ли использовать блок lock (lockObj) {} при заполнении моего ConcurrentDictionary здесь?

Нет, методы в этой структуре данных уже поточно-ориентированы.

1) Будет ли целесообразно устанавливать значение объектов List приватная статика, которую вы блокируете в надежде не вызывать несколько раз перед заполнением базы данных ConcurrentDictionary?

Может быть, особенно если GetList сам по себе не был уже потокобезопасным. За исключением того, что вы предлагаете, не будет работать. Этот экземпляр List<MyBusinessObject> возвращается из GetList, поэтому вы не можете заблокировать то, что еще не существует. Вместо этого вы должны создать отдельный объект только для целей блокировки.

2) Это (# 1 выше) вообще необходимо или это ConcurrentDictionary достаточно умен, чтобы решить это самостоятельно?

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

Кстати, у вашего GetOptionById есть состояние гонки. В блок if одновременно может попасть несколько потоков. Ваш код может попытаться инициализировать словарь более одного раза.

1 голос
/ 23 марта 2012

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

Чтобы избежать этого, нужно заблокировать не саму коллекцию, а условиепроверить внутри GetOptionById.

...