Object Caching Techniques

Applies to: SharePoint Foundation 2010

Many developers use the Microsoft .NET Framework caching objects (for example, System.Web.Caching.Cache) to help take better advantage of memory and increase overall system performance. Many objects are not "thread safe," however, and caching those objects can cause applications to fail and cause unexpected or unrelated user errors.

Note

The caching techniques discussed in this section are different from the custom caching options for Web content management that are discussed in Custom Caching Overview in SharePoint Server 2010 (ECM).

Caching Data and Objects

Caching is a good way to improve system performance. However, you must weigh the benefits of caching against the need for thread safety, because some SharePoint objects are not thread safe and caching causes them to perform in unexpected ways.

Caching SharePoint Objects That Are Not Thread Safe

You might try to increase performance and memory usage by caching SPListItemCollection objects that are returned from queries. In general, this is a good practice; however, the SPListItemCollection object contains an embedded SPWeb object that is not thread safe and should not be cached.

For example, assume the SPListItemCollection object is cached in a thread. As other threads try to read this object, the application can fail or behave strangely because the embedded SPWeb object is not thread safe. For more information about the SPWeb object and thread safety, see the Microsoft.SharePoint.SPWeb class.

The guidance in the following section describes how you can prevent problems that occur when you cache SharePoint objects that are not thread-safe in a multithreaded environment.

Understanding the Potential Disadvantages of Thread Synchronization

You might not be aware that your code is running in a multithreaded environment (by default, Internet Information Services, or IIS, is multithreaded) or how to manage that environment. The following example shows code that is sometimes used to cache Microsoft.SharePoint.SPListItemCollection objects that are not thread safe.

Bad Coding Practice

Caching an object that multiple threads might read

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

The use of the cache in the preceding example is functionally correct; however, because the ASP.NET cache object is thread safe, it introduces potential performance problems. (For more information about ASP.NET caching, see the Cache class.) If the query in the preceding example takes 10 seconds to complete, many users could try to access that page simultaneously during that amount of time. In this case, all of the users would run the same query, which would attempt to update the same cache object. If that same query runs 10, 50, or 100 times, with multiple threads trying to update the same object at the same time—especially on multiprocess, hyperthreaded computers—performance problems would become especially severe.

To prevent multiple queries from accessing the same objects simultaneously, you must change the code as follows.

Applying a Lock

Checking for 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

You can increase performance slightly by placing the lock inside the if(oListItems == null) code block. When you do this, you do not need to suspend all threads while checking to see if the data is already cached. Depending on the time it takes the query to return the data, it is still possible that more than one user might be running the query at the same time. This is especially true if you are running on multiprocessor computers. Remember that the more processors that are running and the longer the query takes to run, the more likely putting the lock in the if() code block will cause problems. To ensure that another thread has not created oListItems before the current thread has a chance to work on it, you could use the following pattern.

Applying a Lock

Rechecking for 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

If the cache is already populated, this last example performs as well as the initial implementation. If the cache is not populated and the system is under a light load, acquiring the lock will cause a slight performance penalty. This approach should significantly improve performance when the system is under a heavy load, because the query is executed only once instead of multiple times, and queries are usually expensive compared with the cost of synchronization.

The code in these examples suspends all other threads in a critical section running in IIS, and prevents other threads from accessing the cached object until it is completely built. This addresses the thread synchronization issue; however, the code is still not correct because it is caching an object that is not thread safe.

To address thread safety, you can cache a DataTable object that is created from the SPListItemCollection object. You would modify the previous example as follows so that your code gets the data from the DataTable object.

Good Coding Practice

Caching a DataTable object

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

For more information and examples of using the DataTable object, and other good ideas for developing SharePoint applications, see the reference topic for the DataTable class.