オブジェクト キャッシュ技法
最終更新日: 2010年1月17日
適用対象: SharePoint Foundation 2010
多くの開発者は、メモリを有効利用してシステム全体のパフォーマンスを向上させるために、Microsoft .NET Framework のキャッシュ オブジェクト (System.Web.Caching.Cache など) を使用します。ただし、"スレッド セーフ" ではないオブジェクトは多数あり、そのようなオブジェクトをキャッシュすると、アプリケーションが失敗し、予期しないまたは無関係のユーザー エラーが発生することがあります。
注意
このセクションで説明するキャッシュ技法は、「カスタム キャッシュの概要」で説明している Web コンテンツ管理のカスタム キャッシュ方法とは異なります。
データとオブジェクトのキャッシュ
キャッシュはシステム パフォーマンス向上に役立ちます。ただし、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 回と実行されると、複数のスレッドから同じオブジェクトの更新が同時に試行されることによって、(マルチプロセスでハイパースレッドのコンピューターの場合は特に) パフォーマンスの問題が深刻になります。
同じオブジェクトが複数のクエリから同時にアクセスされないようにするには、このコードを以下のように変更します。
ロックを適用する
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
既にキャッシュが行われている場合は、このコード例のパフォーマンスは最初のコード例と同様です。キャッシュがまだ行われていない場合、システムにかかっている負荷が小さいときは、ロックを適用することによってパフォーマンスは若干低下します。この方法でパフォーマンスが顕著に向上するのは、システムに大きな負荷がかかっている場合です。クエリが複数回ではなく 1 回だけ実行されることになり、通常、クエリによる負荷のほうが同期による負荷より大きいからです。
これらのコード例では、IIS で実行されている他のすべてのスレッドをクリティカルな部分で保留し、キャッシュされたオブジェクトの作成が完了するまで、他のスレッドからそのオブジェクトにアクセスできないようにしています。これによって、スレッドの同期の問題は解決しますが、スレッド セーフではないオブジェクトをキャッシュしているという点でこれらのコードはまだ適切ではありません。
スレッド セーフに対応するには、SPListItemCollection オブジェクトから作成する DataTable オブジェクトをキャッシュします。前のコード例を以下のように変更して、コードで 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 クラスのリファレンス トピックを参照してください。