シングルトンおよび静的コンストラクターについて

当初、著者はこの記事を次のように呼び出したいと思っていました。著者が知らない理由で複数行の名前が現代のコンピューターコミュニティに根付かなかったため、彼(著者)はこの名前を短くして、元の意味をひどくゆがめることにしました。

-------------------------

シングルトンパターンの実装には一般に2つの目標があります。まず、マルチスレッド.Netの世界で複数のインスタンスが作成されないように、実装はスレッドセーフでなければなりません。 第二に、この実装は、「潜在的に」高価なオブジェクトを事前に作成したり、まったく必要ない場合に作成したりしないように、「遅延」する必要があります。 しかし、シングルトンの実装に関する記事を読む際にはマルチスレッドが主な注意を払われているため、「怠laz」には欲求が欠けていることがよくあります。



静的フィールド初期化子に基づいた、Singleton(*)パターンの最も単純で最も一般的な実装の1つを見てみましょう。

public sealed class Singleton
{
private static readonly Singleton instance = new Singleton();

// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Singleton()
{}

private Singleton()
{}

public static Singleton Instance { get { return instance; } }
}

* This source code was highlighted with Source Code Highlighter .


この実装のスレッドセーフは、1つのドメイン内の静的コンストラクター(つまり、型初期化子)が複数回呼び出されないことが保証されているという事実に基づいています。 その場合、開発者はスレッドセーフをさらに安全にするために耳を傾ける必要はありません。 ほとんどの開発者(そして私自身を含む最近まで)はこれについて落ち着きました。なぜなら、シングルトンで起こりうる主な問題をすでに決定しているためです。 そして、このコメントはまったく理解できないため、空の静的コンストラクターは、実際のアプリケーションの多くの実装に到達しません。

静的コンストラクターとフィールド初期化子



静的コンストラクターは型を初期化するように設計されたもので、静的または非静的メンバーにアクセスする前、およびクラスのインスタンスを作成する前に呼び出す必要があります。 ただし、C#のクラスに静的コンストラクターの明示的な宣言が含まれていない場合、コンパイラーはbeforeFieldInit属性でマークします。これにより、型は「リラックスした」方法で初期化できることがランタイムに通知されます。 ただし、実践が示すように、バージョン4までの.Net Frameworkでは、この動作は「遅延」ではなく、何でも呼び出すことができます。

それでは、次のコードを見てみましょう。

class Singleton
{
//static Singleton()
//{
// Console.WriteLine(".cctor");
//}

public static string S = Echo( "Field initializer" );

public static string Echo( string s)
{
Console .WriteLine(s);
return s;
}
}

class Program
{

static void Main( string [] args)
{
Console .WriteLine( "Starting Main..." );
if (args.Length == 1)
{
Console .WriteLine(Singleton.S);
}
Console .ReadLine();
}
}

* This source code was highlighted with Source Code Highlighter .


この場合、 Singletonクラスには明示的な静的コンストラクターがないため、コンパイラはbeforeFieldInit属性をこの型に追加します。 仕様によれば、静的フィールドのこの初期化では、このフィールドへの最初の呼び出しの前に発生し、この呼び出しのかなり前に発生する可能性があります。 実際には、.Net Framework 3.5以前を使用すると、条件がargsであっても、Mainメソッドを呼び出す前に静的フィールドが初期化されます Legnth == 1は失敗します。 これはすべて、上記のコードを実行すると、次のようになるという事実につながります。

フィールド初期化子

メインの起動...

ご覧のとおり、静的フィールドは初期化されますが、型自体はアプリケーションでは使用されません。 実際には、ほとんどの場合、明示的なコンストラクターがない場合、JITコンパイラーはこの変数が使用されるメソッドを呼び出す直前に静的変数の初期化子を呼び出します。 Singletonクラスの静的コンストラクターのコメントを外すと、ほとんどの開発者が期待するとおりの動作になります。フィールド初期化子は呼び出されず、アプリケーションの起動時に画面に Start Main ...」という行が1行だけ表示されます。



静的コンストラクターが呼び出されている間、開発者を縛ることはできません。 「法の手紙」に従うと、上記の例(明示的な型コンストラクターなし)で、 Singleton変数になる可能性が非常に高くなります Sは、 Singletonクラスをインスタンス化するとき、およびS フィールドを使用しない静的メソッドを呼び出すときは初期化されませんが、 S フィールドを使用して静的関数を呼び出すときは初期化されます。 これはまさにbeforeFieldInitフラグの定義で最初に定められた動作ですが、C#言語仕様では、呼び出しの正確な時間は実装によって決定されると明確に述べられています。 したがって、たとえば、.Net Framework 4で上記のソースフラグメントを(明示的な静的コンストラクターなしで)開始すると、より期待される動作が得られます。S フィールド 初期化 されません! 詳細については、記事の最後にあるサイトリンクをご覧ください。

静的コンストラクターとデッドロック



指定された型の静的コンストラクターは、アプリケーションドメインで1回しか呼び出されないため、CLRは何らかのロック内で呼び出します。 次に、静的コンストラクターを実行しているスレッドが別のスレッドの完了を待機すると、同じ内部CLRロックをキャプチャしようとすると、古典的なデッドロックが発生します。

理想的な条件下で同様の状況を再現するのは非常に簡単です。このためには、静的コンストラクターで、新しいスレッドを作成し、その実行を待機するだけで十分です。

class Program
{
static Program()
{
var thread = new Thread(o => { });
thread.Start();
thread.Join();
}

static void Main()
{
// ,
//
// Program
}
}

* This source code was highlighted with Source Code Highlighter .


さらに、CLIの仕様に目を向けると、静的コンストラクター内でブロッキング操作を明示的または暗黙的に呼び出すと、デッドロックが発生する可能性があると言われています。 実際には、これは、たとえば、場合によっては名前付きミューテックスの使用が原因で、静的コンストラクター内のスレッドをブロックしようとすると、デッドロックが発生する可能性があることを意味します。

実際のアプリケーションのバグ



静的コンストラクターでフィールド初期化子とデッドロックを呼び出す時間に関連するこのすべてのナンセンスは、実際のアプリケーションでは指から吸い込まれそうにないようです。 正直に言うと、1週間前に1つの非常に不快なバグをデバッグするのに1日を費やす前に、私は同じ意見でした。

だから、ここに私が直面している本当の問題の症状があります。 コンソールモードで最適に機能するサービスがあり、デバッグで組み合わせると同様に機能します。 ただし、リリースで収集すると、1回起動します。1回正常に起動し、2回目はタイムアウトにより起動がクラッシュします(デフォルトでは、サービスが30秒以内に起動しない場合、SCMはプロセスを強制終了します)。

デバッグの結果、次のことがわかりました。 (1)パフォーマンスカウンターが作成されるコンストラクターにサービスクラスがあります。 (2)サービスクラスは、明示的な静的コンストラクタなしで静的フィールドを初期化することによりシングルトンとして実装されます。(3)このシングルトンは、コンソールモードでサービスを開始するMainメソッドで直接使用されました。

//
partial class Service : ServiceBase
{
// "" .
public static readonly Service instance = new Service();
public static Service Instance { get { return instance; } }

public Service()
{
InitializeComponent();

//
var counters = new CounterCreationDataCollection();

if (PerformanceCounterCategory.Exists(category))
PerformanceCounterCategory.Delete(category);

PerformanceCounterCategory.Create(category, description,
PerformanceCounterCategoryType.SingleInstance, counters);
}

//
public void Start()
{}

const string category = "Category" ;
const string description = "Category description" ;
}
// Program
static void Main( string [] args)
{
if (args[0] == "--console" )
Service.Instance.Start();
else
ServiceBase.Run( new Service());

}

* This source code was highlighted with Source Code Highlighter .


Serverクラスには明示的な静的コンストラクターが含まれておらず、C#コンパイラーがbeforeFieldInitフラグを追加するため、 Mainメソッドが呼び出される前にServiceクラスのコンストラクターが呼び出されます。 同時に、名前付きミューテックスを使用してパフォーマンスカウンターのカテゴリを作成します。これにより、特定の条件下でアプリケーションのデッドロックが発生します。最初の実行中、指定したカテゴリはまだシステムにないため、 Existsメソッドはfalseを返し、 Createメソッドは成功します。 次回の実行時に、 Existsメソッドはtrueを返し、 Deleteメソッドは成功しますが、 Createメソッドは永久にハングします。 問題が見つかった後、ソリューションが正確に13秒かかったことは明らかです。静的コンストラクターをServiceクラスに追加します。

おわりに



実際のアプリケーションのバグの例は、C#言語の落とし穴とよく知られているパターンとイディオムの適切な使用に関する記事は、理論家のナンセンスでフィクションではないことを示唆しています(**)。そのような記事の多くは、現実の世界に詰め込まれたバンプに基づいています。 今日、明日、シングルトン実装曲線で問題が発生する可能性があります-明日の翌日、Thread.Abortでスレッドを破り、システムの不一致を取得します(***)。 これらの問題はすべて非常に現実的なものであり、その基礎にある原則を理解することで、特に悪いバグを探す日を節約できます。

サイトリンク





-------------------------

(*)シングルトンパターンの2つの非常に一般的な実装は、(1)ダブルチェックロック、および(2).Net Framework 4で登場したLazy < T>タイプの使用です。

(**)私の同僚の一人は、本や記事を読むことは理にかなっていないと固く信じています。なぜならそれらはこのソフトウェア開発で何も理解していない人によって書かれているからです。 したがって、彼は「実際の」シングルトン実装は1つだけであり、IDisposableインターフェイスを実装するすべてのクラスに空のファイナライザーを追加する必要があると考えています。

(***)可変の意味のある型にどんな種類の問題が潜んでいるのかがおもしろい場合、前の記事「可変の意味のある型の危険性について」が適切かもしれませんが、Thread.Abortを呼び出すことについて何がそんなに悪いのか疑問に思っているなら、注: 「Thread.Abortを呼び出すことの危険性」 、およびChris Sellによる興味深い記事の翻訳「Learning ThreadAbortExcpetion Using Rotor」

Source: https://habr.com/ru/post/J125421/


All Articles