Состояние гонки ASP.NET/статического класса?

У меня есть приложение ASP.NET с большим количеством динамического содержимого. Содержимое одинаково для всех пользователей, принадлежащих конкретному клиенту. Чтобы уменьшить количество обращений к базе данных, необходимых для каждого запроса, я решил кэшировать данные на уровне клиента. Я создал статический класс ("ClientCache") для хранения данных.
Наиболее часто используемый метод класса — это "GetClientData", который возвращает объект ClientData, содержащий все сохраненные данные для конкретного клиента. Однако ClientData загружается лениво: если запрошенные данные клиента уже кэшированы, вызывающая сторона получает кэшированные данные; в противном случае данные извлекаются, добавляются в кеш и затем возвращаются вызывающей стороне.

В конце концов я начал получать периодические сбои в методе GetClientData в строке, где объект ClientData добавляется в кеш. Вот тело метода:

public static ClientData GetClientData(Guid fk_client)
{
    if (_clients == null)
        _clients = new Dictionary<Guid, ClientData>();

    ClientData client;
    if (_clients.ContainsKey(fk_client))
    {
        client = _clients[fk_client];
    }
    else
    {
        client = new ClientData(fk_client);
        _clients.Add(fk_client, client);
    }
    return client;
}

Текст исключения всегда выглядит примерно так: «Объект с таким ключом уже существует». Конечно, я пытался написать код так, чтобы просто нельзя было добавить клиента в кеш, если он уже есть.

На данный момент я подозреваю, что у меня есть состояние гонки, и метод выполняется дважды одновременно, что может объяснить, как произойдет сбой кода. Однако меня смущает то, как этот метод вообще может выполняться дважды одновременно. Насколько мне известно, любое приложение ASP.NET обрабатывает только один запрос за раз (поэтому мы можем использовать HttpContext.Current).

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


person mschaad    schedule 21.02.2009    source источник


Ответы (4)


Если приложение ASP.NET обрабатывает только один запрос за раз, все сайты ASP.NET столкнутся с серьезными проблемами. ASP.NET может обрабатывать десятки одновременно (обычно 25 на ядро ​​ЦП).

Вы должны использовать кэш ASP.NET вместо использования собственного словаря для хранения вашего объекта. Операции с кешем потокобезопасны.

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

Изменить:

В комментарии к этому ответу говорится: -

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

Стоит отметить, что если мы чувствуем, что нам нужно сделать такую ​​операцию атомарной, то кеш, вероятно, не является подходящим местом для ресурса.

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

person AnthonyWJones    schedule 21.02.2009
comment
Спасибо за объяснение. Я знал о кэшировании вывода ASP.NET, но не знал об объекте Cache. Кэширующий API решит мою проблему намного чище. - person mschaad; 03.03.2009
comment
Только атомарные операции с кешем являются потокобезопасными. Если вы делаете что-то вроде проверки существования ключа, а затем добавляете его, это НЕ является потокобезопасным и может привести к перезаписи элемента. - person Robert C. Barth; 04.03.2009

Это очень легко исправить:

private _clientsLock = new Object();

public static ClientData GetClientData(Guid fk_client)
{
  if (_clients == null)
    lock (_clientsLock)
      // Check again because another thread could have created a new 
      // dictionary in-between the lock and this check
      if (_clients == null) 
        _clients = new Dictionary<Guid, ClientData>();

  if (_clients.ContainsKey(fk_client))
    // Don't need a lock here UNLESS there are also deletes. If there are
    // deletes, then a lock like the one below (in the else) is necessary
    return _clients[fk_client];
  else
  {
    ClientData client = new ClientData(fk_client);

    lock (_clientsLock)
      // Again, check again because another thread could have added this
      // this ClientData between the last ContainsKey check and this add
      if (!clients.ContainsKey(fk_client))
       _clients.Add(fk_client, client);

    return client;
  }
}

Имейте в виду, что всякий раз, когда вы возитесь со статическими классами, у вас могут возникнуть проблемы с синхронизацией потоков. Если есть какой-то статический список на уровне класса (в данном случае _clients, объект Dictionary), ОПРЕДЕЛЕННО возникнут проблемы с синхронизацией потоков.

person Robert C. Barth    schedule 22.02.2009
comment
спасибо за подробное объяснение. Это произошло в спешке, поэтому я приступил к решению блокировки, ожидая, пока мои пробелы в знаниях будут заполнены здесь. Единственная реальная разница между моим кодом и вашим заключается в том, что мне пришлось заблокировать весь класс, так как класс статичен. - person mschaad; 03.03.2009
comment
Блокировка класса может привести к серьезной конкуренции за ресурсы; ты не хочешь этого делать. - person Robert C. Barth; 04.03.2009

Ваш код действительно предполагает, что в каждый момент времени в функции находится только один поток.

Этого просто не будет в ASP.NET.

Если вы настаиваете на этом, используйте статический семафор, чтобы заблокировать область вокруг этого класса.

person Joshua    schedule 21.02.2009

вам нужна многопоточность и минимальная блокировка.
см. блокировку с двойной проверкой (http://en.wikipedia.org/wiki/Double-checked_locking)

напишите просто с TryGetValue.


public static object lockClientsSingleton = new object();

public static ClientData GetClientData(Guid fk_client)
{
    if (_clients == null) {
        lock( lockClientsSingleton ) {
            if( _clients==null ) {
                _clients = new Dictionary``();
            }
        }
    }
    ClientData client;
    if( !_clients.TryGetValue( fk_client, out client ) )
    {
        lock(_clients) 
        {
            if( !_clients.TryGetValue( fk_client, out client ) ) 
            {
                client = new ClientData(fk_client)
                _clients.Add( fk_client, client );
            }
        }
    }
    return client;
}
person kazuk    schedule 19.10.2009