はじめに
今回はスレッドセーフなシングルトンを実装してみる記事になります。
Unityなんかでは頻繁に使われているシングルトンパターンですが、マルチスレッドで処理をする場合には落とし穴があったりします。
どういった落とし穴があるのかということから、その解決方法はどういったものがあるかを考えていきたいと思います。
では早速みていきましょう。
シングルトン
null
かどうかによって判定する一般的なシングルトンをマルチスレッドで動作させてみます。
using System; using System.Collections.Generic; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; class Program { static void Main(string[] args) { // 5個のスレッドで並列に実行 for(int i = 0; i < 5; i++) { _ = Task.Run(() => { for (int j = 0; j < 3; j++) { Model.Instance.Queue.Enqueue(j); Thread.Sleep(1); } }); } var count = 0; while(count < 15) { int item = 0; if(Model.Instance.Queue.TryDequeue(out item)) { Console.Write(item); count++; } Thread.Sleep(1); } } } public class Model { private static Model instance = null; public static Model Instance => instance ?? (instance = new Model()); public ConcurrentQueue<int> Queue { get; } public Model() { Console.WriteLine("Constructor called."); Queue = new ConcurrentQueue<int>(); } }
コードをみていただくと、正しく動作しそうな気もしますが結果はめちゃくちゃです。
Constructor called. Constructor called. Constructor called. Constructor called. Constructor called. Constructor called. 01111122222
コンストラクタが6回呼ばれてしまっていますね。
これはスレッドセーフがでないことに由来し、スレッド毎にコンストラクタが呼ばれてしまったというわけです。
スレッドセーフなシングルトン
先ほどのコードのModel
クラスを少しだけ変えてみます。
public class Model { private static Lazy<Model> instance = new Lazy<Model>(); public static Model Instance => instance.Value; public ConcurrentQueue<int> Queue { get; } public Model() { Console.WriteLine("Constructor called."); Queue = new ConcurrentQueue<int>(); } }
Constructor called. 000001111122222
こちらは一度だけコンストラクタが呼ばれ、コードも正しく終了することができました。
このLazy
は遅延初期化のサポートをするクラスです。
Lazy.Value
プロパティにアクセスすることで(初回のときのみ)インスタンスの作成・コンストラクタの実行がされます。
このマルチスレッドに対応したという意味からスレッドセーフであると呼ばれています。
さいごに
この記事はC#向けに書いたのですが結構Unityにも関係しそうな箇所なので、Unity向けに新しく記事を書こうかなと思ってます。
もしそちらに興味があるかたは是非みてみてください。
ではまた。