さて、前回は一番単純なSingletonの実装を紹介しました。そして、そのなかで、
以下のようにマルチスレッドで動かす場合、1つだけであるはずのインスタンスが、複数生成されてしまうことがあります。
~中略~
ごらんのように、インスタンスが2つ生成された結果、ログ出力パスが変わってしまいます。
この問題を解消する方法はないのでしょうか?
実はあります。
と書きました。
まずは、なぜインスタンスが2つ作成されるかを考えてみましょう。そのために、前回のコードの、インスタンス取得部分のコードを再掲します。
C#
/// <summary>
/// プロパティ インスタンス
/// </summary>
/// <remarks>自身の唯一のインスタンスを返す</remarks>
public static SingletonLogger Instance
{
get
{
if ( _uniqueLogger == null )
{
_uniqueLogger = new SingletonLogger();
}
return _uniqueLogger;
}
}
VB
''' <summary>
''' プロパティ インスタンス
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks>自身の唯一のインスタンスを返す</remarks>
Public Shared ReadOnly Property Instance() As SingletonLogger
Get
If _uniqueLogger Is Nothing Then
_uniqueLogger = New SingletonLogger
End If
Return _uniqueLogger
End Get
End Property
このコードで何が問題かというと、C#、VBともに9行目でインスタンスがないことを確認した後、10行目で新しいインスタンスを作成するのですが、コンストラクタが重い場合、インスタンスが生成される前に他のスレッドによって9行目が再び実行され、このときはやはりインスタンスがないので、そのまま後に実行されたスレッドでも10行目が実行されてしまうのです。
これを解決する方法には大きく2つあるのですが、今回は有名な「二重チェック」の方法を紹介します。さっそくコードを見てみましょう。
C#
/// <summary>
/// ログ出力クラス
/// </summary>
public class SingletonLogger
{
/// <summary>
/// ログ出力パス
/// </summary>
private string _logPath;
/// <summary>
/// プロパティ ログ出力パス
/// </summary>
public string LogPath
{
get
{
return _logPath;
}
}
/// <summary>
/// コンストラクタ
/// </summary>
/// <remarks>new でインスタンス化できないよう、privateでコンストラクタを定義</remarks>
private SingletonLogger()
{
// 重い初期化処理
for ( var i = 0; i < 100000000; i++ )
{
}
_logPath = DateTime.Now.ToString("yyyyMMddhhmmssfff") + ".log";
}
/// <summary>
/// 自身の唯一のインスタンス
/// </summary>
private volatile static SingletonLogger _uniqueLogger;
/// <summary>
/// ロック用のオブジェクト
/// </summary>
private static object lockObj = new object();
/// <summary>
/// プロパティ インスタンス
/// </summary>
/// <remarks>自身の唯一のインスタンスを返す</remarks>
public static SingletonLogger Instance
{
get
{
if ( _uniqueLogger == null )
{
lock ( lockObj )
{
if ( _uniqueLogger == null )
{
_uniqueLogger = new SingletonLogger();
}
}
}
return _uniqueLogger;
}
}
/// <summary>
/// ログ出力
/// </summary>
/// <param name="message"></param>
public void WriteLog(string message)
{
// ログ出力処理
// ・・・
}
}
VB
''' <summary>
''' ログ出力クラス
''' </summary>
''' <remarks></remarks>
Public Class SingletonLogger
''' <summary>
''' ログ出力パス
''' </summary>
''' <remarks></remarks>
Private _logPath As String
''' <summary>
''' プロパティ ログ出力パス
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public ReadOnly Property LogPath() As String
Get
Return _logPath
End Get
End Property
''' <summary>
''' コンストラクタ
''' </summary>
''' <remarks>new でインスタンス化できないよう、privateでコンストラクタを定義</remarks>
Private Sub New()
' 重い初期化処理
For i As Integer = 0 To 100000000
Next i
_logPath = DateTime.Now.ToString("yyyyMMddhhmmssfff") & ".log"
End Sub
''' <summary>
''' 自身の唯一のインスタンス
''' </summary>
''' <remarks></remarks>
Private Shared _uniqueLogger As SingletonLogger
Private Shared _lockObj As New Object()
''' <summary>
''' プロパティ インスタンス
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks>自身の唯一のインスタンスを返す</remarks>
Public Shared ReadOnly Property Instance() As SingletonLogger
Get
If _uniqueLogger Is Nothing Then
SyncLock (_lockObj)
If _uniqueLogger Is Nothing Then
_uniqueLogger = New SingletonLogger
End If
End SyncLock
End If
Return _uniqueLogger
End Get
End Property
End Class
実行結果は次の通り。
実行結果
20080724111338001.log
20080724111338001.log
この方法のポイントは以下の通り。
- ロック用オブジェクトを用意する。(C#の44行目、VBの44行目)
- Instanceプロパティにアクセスされた際、インスタンスの有無を確認後、C#はlockステートメント、VBはSyncLockステートメントを使い、1.で用意したオブジェクトをロックする。(C#の56行目、VBの55行目)
これにより、後に実行されたスレッドは先に実行されたスレッドにてロックが解除されるまで待つこととなる。
- ロックした後、もう一度インスタンスの有無を確認した後、インスタンスがなければ生成する。(C#の58~60行目、VBの56行目から57行目)
なお、C#のコードの39行目にて、_uniqueLoggerフィールドを宣言する際、「volatile」というキーワードをつけています。これは、このキーワードを付けられたフィールドは、キャッシュメモリ内に領域を確保しなくなるため、RAMとキャッシュで値が食い違うという現象が発生しなくなります。
が、CLRのメモリ管理ではこの問題を解決しているため、「volatile」は無くても問題ありません。(Monoなど他のプラットフォームでは必要です、たぶん。)
#この辺りのことは、「プログラミング .NET Framework 第2版」のp.693、「24.3.8 ロックのための有名な二重チェックテクニック」に詳しく書かれています。
ところで、上記書籍の該当箇所にこんなことも書いてあります。
このテクニックが有名なのは、特に面白かったり便利だったりするからではありません。これが有名な理由は、これについて書かれた文献が多いからです。このテクニックはJavaで非常によく使われています。そして、Javaではこれがどこででも動作するわけではないことが後に明らかになりました。この問題を説明している有名な文書が次のWebページにあります。
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
この問題って今はどうなってるんでしょうか?教えて!Javaのエロい人!
#次回はもうひとつの方法について紹介します。