スレッドセーフなSingleton
CoddyのC#ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 52/70。
シングルトンパターン(Singleton pattern)は、アプリケーション全体でクラスのインスタンスが1つだけであることを保証し、それに対するグローバルなアクセスポイントを提供します。これは、設定情報、ロギングサービス、データベース接続などの共有リソースを管理するのに役立ちます。
基本的なシングルトンは、外部からのインスタンス化を防ぐためにプライベートコンストラクタを使用し、アクセスを提供するために静的プロパティを使用します:
public class Logger
{
private static Logger _instance;
private static readonly object _lock = new object();
private Logger() { } // プライベートコンストラクタ
public static Logger Instance
{
get
{
lock (_lock)
{
if (_instance == null)
_instance = new Logger();
return _instance;
}
}
}
public void Log(string message) => Console.WriteLine(message);
}lock ステートメントはスレッドセーフを保証します。複数のスレッドが同時に Instance にアクセスしようとした場合、一度に1つのスレッドだけがクリティカルセクションに入ることができます。これがないと、2つのスレッドが両方とも _instance を null と見なし、別々のインスタンスを作成してしまう可能性があります。
C#は、静的初期化を使用した、よりシンプルなスレッドセーフなアプローチを提供します:
public class Logger
{
private static readonly Logger _instance = new Logger();
private Logger() { }
public static Logger Instance => _instance;
public void Log(string message) => Console.WriteLine(message);
}このバージョンは、CLRが静的フィールドの初期化が一度だけ行われることを保証しているため、スレッドセーフです。どちらのアプローチでも、常に同じインスタンスが取得されることが保証されます:
Logger.Instance.Log("First call");
Logger.Instance.Log("Same instance");チャレンジ
簡単Singleton パターンを使用して、スレッドセーフな設定マネージャーを構築しましょう。実際のアプリケーションでは、設定項目はプログラム全体からアクセス可能である必要があり、かつインスタンスが1つだけ存在することを保証する必要があります。これは Singleton の完璧なユースケースです。
コードは2つのファイルに分けて構成します:
ConfigManager.cs:Configuration名前空間内に、Singleton パターンを実装したConfigManagerクラスを作成します。この設定マネージャーは以下の要件を満たす必要があります:- 単一のインスタンスを保持するための private static フィールドを持つこと
- 外部からのインスタンス化を防ぐための private コンストラクタを使用すること
- 単一のインスタンスを返す static な
Instanceプロパティを提供すること(スレッドセーフのために、よりシンプルな静的初期化アプローチを使用してください) "MyApp"で初期化された private な文字列フィールド_appNameを含めること- アプリ名を更新するための
SetAppName(string name)メソッドを持つこと - 現在のアプリ名を返す
GetAppName()メソッドを持つこと
Program.cs: メインファイルで、インスタンスに複数回アクセスし、すべての参照で変更が保持されていることを示すことで、Singleton が正しく機能することを実証します。
1つの入力を受け取ります:
- 設定する新しいアプリケーション名(例:
ProductionServer)
プログラム内での手順:
ConfigManagerインスタンスを取得し、デフォルトのアプリ名を表示します。- 再度インスタンスを取得し(別の変数に代入)、それを使用して入力された新しいアプリ名を設定します。
- 3回目にインスタンスを取得し、アプリ名を表示して、すべての参照が同じデータを共有していることを証明します。
例えば、入力が ProductionServer の場合、出力は以下のようになります:
MyApp
ProductionServerこれは Singleton の核心的な利点を示しています。何度 Instance にアクセスしても、常に共有された状態を持つ同じオブジェクトが得られます。静的初期化アプローチは、複数のスレッドが同時にアクセスしたとしても、CLR が static フィールドが一度だけ初期化されることを保証するため、スレッドセーフが担保されます。
チートシート
Singleton パターンは、アプリケーション全体でクラスのインスタンスが1つだけであることを保証し、それに対するグローバルなアクセスポイントを提供します。
スレッドセーフのために明示的なロックを使用した基本的な Singleton:
public class Logger
{
private static Logger _instance;
private static readonly object _lock = new object();
private Logger() { } // Private constructor
public static Logger Instance
{
get
{
lock (_lock)
{
if (_instance == null)
_instance = new Logger();
return _instance;
}
}
}
public void Log(string message) => Console.WriteLine(message);
}lock ステートメントは、一度に1つのスレッドだけがクリティカルセクションに入ることを許可することで、スレッドセーフを保証します。
静的初期化を使用した、よりシンプルなスレッドセーフな Singleton:
public class Logger
{
private static readonly Logger _instance = new Logger();
private Logger() { }
public static Logger Instance => _instance;
public void Log(string message) => Console.WriteLine(message);
}このアプローチは、CLR が静的フィールドの初期化が一度だけ行われることを保証するため、スレッドセーフです。
使用例 - 常に同じインスタンスを返します:
Logger.Instance.Log("First call");
Logger.Instance.Log("Same instance");自分で試してみよう
using System;
using Configuration;
class Program
{
public static void Main(string[] args)
{
// 入力を読み込む - 新しいアプリケーション名
string newAppName = Console.ReadLine();
// TODO: ConfigManager インスタンスを取得し、デフォルトのアプリ名を出力する
// TODO: インスタンスを再度取得し (別の変数に)、それを使用して新しいアプリ名を設定する
// TODO: インスタンスを3回目として取得し、すべての参照が同じデータを共有していることを証明するためにアプリ名を出力する
}
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。