質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

解決済

2回答

7905閲覧

Singleton が複数回初期化されてしまう

aglkjggg

総合スコア769

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

0グッド

0クリップ

投稿2016/10/25 03:15

編集2016/10/25 03:17

###前提・実現したいこと
Assetというクラスでアセットファイルの読み込みを行っています。
アセットファイルはアプリ起動後ずっと利用するので、
一度読み込んだ後はメモリ上に残しておきたいと思っています。

調べると、GoFデザインパターのうちSingletonというのが使えるということが分かり、
以下のように実装しました

C#

1public class Asset 2{ 3 private static Asset _instance; 4 public static Asset Instance 5 { 6 get 7 { 8 if (_instance == null) 9 { 10 _instance = new Asset(); 11 } 12 return _instance; 13 } 14 } 15 16 private Asset() 17 { 18 // コンストラクタでアセットファイルの読み込みを行う 19 } 20}

しかし、アプリ起動後は以下のコードのように複数スレッドを同時に生成するような動きをするため、
Singletonであっても同時に複数回Assetクラスが初期化されてしまいました。
その結果、メモリが一気に大量に消費されるような動きとなってしまっています。

C#

1static void Main(string[] args) 2{ 3 Task.Run(() => 4 { 5 Enumerable.Range(0, 10) 6 .ToList() 7 .ForEach(x => AsyncMain()); 8 }); 9}

調べていくとawaitを利用した関数の中で初めて初期化するとこのような動きになることが分かりました。

C#

1static async void AsyncMain() 2{ 3 // 複数回初期化されない 4 var x = Asset.Instance; 5 6 // await の中だと複数回初期化される 7 // var y = await Something.Do(Asset.Instance); 8}

MSDNによると静的コンストラクタは最高で一度だけ実行されると書かれていました。
The static constructor for a class executes at most once in a given application domain.
https://msdn.microsoft.com/en-us/library/aa645612.aspx

質問

なぜawaitの中で初めてインスタンスの初期化を行うとSingletonの本来の動きにならない(複数回初期化されてしまう)のでしょうか?

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ベストアンサー

まず最初に
静的コンストラクタを勘違いされているようです。
静的コンストラクタとは以下のような書き方をするもので、
プログラム全体で数えて初めてそのクラスが作られた時に一度だけ実行されます。
追記:staticメンバが初めて参照される場合でも呼ばれるようです。その場合でもプログラム全体で実行されるのは一度だけです。

C#

1class MyClass 2{ 3 static MyClass() // 通常のコンストラクタの前にstaticが付く 4 { 5 } 6}

今回の件の要点は、スレッドセーフの話です。

スレッドセーフな作りになってないので、複数のスレッドでAssetが同時に呼ばれると最初の_instanceのnewが終わるまでの間に他のスレッドがif文をすり抜けてしまう事が起こり得ます。
(newが完了するまで_instanceはnullのままなので)

解決方法はいろいろあります。
例としてロックオブジェクトを使うやり方を紹介します。
ただし、このやり方は簡単ですがベストなやり方というわけではないので、
興味を持ったら別の方法も調べて試してみてください。

C#

1public class Asset 2{ 3 private static object lockObject = new object(); // lockオブジェクトを作成 4 private static Asset _instance; 5 public static Asset Instance 6 { 7 get 8 { 9 lock (lockObject) // このスコープ内は一つのスレッドしか通過できず、そのスレッドが完了するまで他のスレッドは待ち状態になる 10 { 11 if (_instance == null) 12 { 13 _instance = new Asset(); 14 } 15 } 16 return _instance; 17 } 18 } 19 20 private Asset() 21 { 22 // コンストラクタでアセットファイルの読み込みを行う 23 } 24}

投稿2016/10/25 03:46

編集2016/10/25 04:26
ishi9

総合スコア1294

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

aglkjggg

2016/10/25 05:05 編集

ありがとうございます。 正しい静的コンストラクタに書き直したところ、期待する動作となりました。 MSDNには最大で1度だけ実行と書いてあるので、 静的コンストラクタの場合はあえてロックオブジェクトを利用する必要は無いと考えているのですが、書く必要はありますでしょうか? (実際に静的コンストラクタだけで動かしてみても一度だけ初期化されましたので不要ではないかという気がしました) https://msdn.microsoft.com/en-us/library/aa645612.aspx
ishi9

2016/10/25 07:00

ここにいろいろな方法が紹介されています。 静的コンストラクタを使ったやり方もあり、解説の通りlockする必要はないです。 http://qiita.com/laughter/items/e6be52db15d7326b46b9 私は最初の質問文を見た時、静的コンストラクタは使わないやり方だと思ったので その方向で回答しました。(というか静的コンストラクタを使うやり方もあるんだなぁと調べながら関心しました。あまり見ないやり方だったので)
ishi9

2016/10/25 07:05 編集

ちなみによく見かける実装パターンはこういうのです。質問文のコードもこれと同じなので静的コンストラクタを使うというのは何かの間違いなのかなと思っていました。 http://qiita.com/rohinomiya/items/6bca22211d1bddf581c4
aglkjggg

2016/10/25 07:18

ありがとうございます。 できるだけよく知られている方法で書いたほうがいいと思うので、 基本的なパターンに準拠して書き直しておきます><
ishi9

2016/10/25 07:37

私の紹介した、ただlockオブジェクトでロックするというやり方はいちいちロックスコープを通過するので処理が重いという欠点があったのですが、上記サイトの「Double-checked-lockingイディオムパターン」はそれを解決していますね。 私自身、今回の質問はいろいろ勉強になりました。ありがとうございます。
guest

0

こんにちは。

コンストラクタが静的(static)になっていないと思います。
こんな事情も絡むようです。

また、シングトンの生成時はスレッド安全性の考慮が必要です。
一般にif (xxx) yyy;の構造は、if判定後に他のスレッドに実行権が渡る可能性があるため、その時誤動作しやすいです。

投稿2016/10/25 03:40

Chironian

総合スコア23272

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

aglkjggg

2016/10/25 05:02 編集

ご回答ありがとうございます。 C#のSingletonについての濃厚な記事の紹介もありがとうございます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問