Методы кэширования объектов

Дата последнего изменения: 17 января 2010 г.

Применимо к: SharePoint Foundation 2010

Многие разработчики используют кэширование объектов Microsoft .NET Framework (например, System.Web.Caching.Cache) для улучшения использования памяти и общей производительности системы. При этом многие объекты не являются потокобезопасными, и кэширование таких объектов может привести к сбою приложений и непредвиденным ошибкам, не связанным с действиями пользователей.

ПримечаниеПримечание

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

Кэширование данных и объектов

Кэширование позволяет повысить производительность системы. При этом необходимо сбалансировать преимущества кэширования с необходимостью потокобезопасности, так как некоторые объекты SharePoint не являются потокобезопасными и их кэширование приведет к непредсказуемой работе.

Кэширование объектов SharePoint, которые не являются потокобезопасными

Разработчик может попытаться улучшить производительность и использование памяти, кэшируя объекты SPListItemCollection, возвращенные из запросов. В общем случае это рекомендуемое поведение, но объект SPListItemCollection содержит встроенный объект SPWeb, который не является потокобезопасным, поэтому его не следует кэшировать.

Например, предположим, что объект SPListItemCollection кэшируется в потоке. Попытки других потоков прочитать этот объект могут привести к сбою приложения или его непредвиденному поведению, так как встроенный объект SPWeb не является потокобезопасным. Дополнительные сведения об объекте SPWeb и потокобезопасности см. в описании класса Microsoft.SharePoint.SPWeb.

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

Описание возможных недостатков синхронизации потоков

Разработчик может не знать, что код будет исполняться в многопоточной среде (по умолчанию среда IIS является многопоточной), или не знать, как управлять этой средой. В следующем примере приведен код, который иногда используется для кэширования объектов Microsoft.SharePoint.SPListItemCollection, не являющихся потокобезопасными.

Пример неправильного кода

Кэширование объектов, которые могут читаться несколькими потоками

public void CacheData()
{
   SPListItemCollection oListItems;

   oListItems = (SPListItemCollection)Cache["ListItemCacheName"];
   if(oListItems == null)
   {
      oListItems = DoQueryToReturnItems();
      Cache.Add("ListItemCacheName", oListItems, ..);
   }
}
Public Sub CacheData()
    Dim oListItems As SPListItemCollection

    oListItems = CType(Cache("ListItemCacheName"), SPListItemCollection)
    If oListItems Is Nothing Then
        oListItems = DoQueryToReturnItems()
        Cache.Add("ListItemCacheName", oListItems,..)
    End If
End Sub

Использование кэша в предыдущем примере является функционально правильным, но потокобезопасность объекта кэша ASP.NET может представлять проблемы для производительности (дополнительные сведения о кэшировании ASP.NET см. в описании класса Cache). Если выполнение запроса в предыдущем примере занимает 10 секунд, то в течение этого времени к этой странице могут попытаться обратиться многие пользователи. В этом случае все пользователи будут выполнять один и тот же запрос, который попытается обновить один и тот же объект кэша. Если один и тот же запрос выполняется 10, 50 или 100 раз, а несколько потоков пытаются одновременно обновить один и тот же объект, то проблемы производительности могут иметь особенно серьезный характер — в особенности на многопроцессорных компьютерах с поддержкой технологии Hyperthreading.

Чтобы предотвратить одновременное обращение нескольких запросов к одним и тем же объектам, необходимо изменить код следующим образом.

Применение блокировки

Проверка на значение NULL

private static object _lock =  new object();

public void CacheData()
{
   SPListItemCollection oListItems;

   lock(_lock) 
   {
      oListItems = (SPListItemCollection)Cache["ListItemCacheName"];
      if(oListItems == null)
      {
         oListItems = DoQueryToReturnItems();
         Cache.Add("ListItemCacheName", oListItems, ..);
     }
   }
}
Private Shared _lock As New Object()

Public Sub CacheData()
    Dim oListItems As SPListItemCollection

    SyncLock _lock
        oListItems = CType(Cache("ListItemCacheName"), SPListItemCollection)
        If oListItems Is Nothing Then
            oListItems = DoQueryToReturnItems()
 Cache.Add("ListItemCacheName", oListItems,..)
        End If
    End SyncLock
End Sub

Разработчик может увеличить производительность, поместив блокировку в блок кода if(oListItems == null). В этом случае все потоки необязательно приостанавливать во время проверки состояния кэширования данных. В зависимости от времени, в течение которого запрос возвращает данные, сохраняется вероятность того, что этот запрос может быть одновременно выполнен несколькими пользователями. Это также справедливо при работе на многопроцессорных компьютерах. Чем больше процессоров работает и чем дольше время выполнения запроса, тем больше вероятность того, что помещение блокировки в блок кода if() вызовет возникновение ошибок. Перед тем как текущий поток получит возможность работать с oListItems, можно использовать следующий шаблон, чтобы убедиться в том, что другой поток не создал этот объект.

Применение блокировки

Повторная проверка на значение NULL

private static object _lock =  new object();

public void CacheData()
{
   SPListItemCollection oListItems;
       oListItems = (SPListItemCollection)Cache["ListItemCacheName"];
      if(oListItems == null)
      {
         lock (_lock) 
         {
              // Ensure that the data was not loaded by a concurrent thread 
              // while waiting for lock.
              oListItems = (SPListItemCollection)Cache["ListItemCacheName"];
              if (oListItems == null)
              {
                   oListItems = DoQueryToReturnItems();
                   Cache.Add("ListItemCacheName", oListItems, ..);
              }
         }
     }
}
Private Shared _lock As New Object()

Public Sub CacheData()
    Dim oListItems As SPListItemCollection
    oListItems = CType(Cache("ListItemCacheName"), SPListItemCollection)
    If oListItems Is Nothing Then
        SyncLock _lock
            ' Ensure that the data was not loaded by a concurrent thread 
            ' while waiting for lock.
            oListItems = CType(Cache("ListItemCacheName"), SPListItemCollection)
            If oListItems Is Nothing Then
                oListItems = DoQueryToReturnItems()
                           Cache.Add("ListItemCacheName", oListItems,..)
            End If
        End SyncLock
    End If
End Sub

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

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

Чтобы решить проблемы безопасности потоков, можно кэшировать объект DataTable, созданный объектом SPListItemCollection. Чтобы код получал данные из объекта DataTable, можно изменить предыдущий пример следующим образом.

Рекомендуемый пример кода

Кэширование объекта DataTable

private static object _lock =  new object();

public void CacheData()
{
   DataTable oDataTable;
   SPListItemCollection oListItems;
   lock(_lock)
   {
           oDataTable = (DataTable)Cache["ListItemCacheName"];
           if(oDataTable == null)
           {
              oListItems = DoQueryToReturnItems();
              oDataTable = oListItems.GetDataTable();
              Cache.Add("ListItemCacheName", oDataTable, ..);
           }
   }
}
Private Shared _lock As New Object()

Public Sub CacheData()
    Dim oDataTable As DataTable
    Dim oListItems As SPListItemCollection
    SyncLock _lock
        oDataTable = CType(Cache("ListItemCacheName"), DataTable)
        If oDataTable Is Nothing Then
            oListItems = DoQueryToReturnItems()
            oDataTable = oListItems.GetDataTable()
            Cache.Add("ListItemCacheName", oDataTable,..)
        End If
    End SyncLock
End Sub

Дополнительные сведения и примеры использования объекта DataTable, а также другую информацию по разработке приложений SharePoint см. в справочных материалах о классе DataTable.