マルチスレッド対応のシングルトン実装（遅延初期化／JPCERT適合／非同期）

<sup>更新履歴： C#「`Lazy<T>` はあるのに `AsyncLazy<T>` が無いのはなぜ？ とよく聞かれるが実装するつもりはない」とのことで、`Lazy<T>` を使った非同期での遅延初期化を行う場合は注意が必要というお話。</sup><br/>&nbsp;



最近は `○○.Shared` とか `△△.Instance` とかよく見ますね。自分も静的クラスではなくこのパターンを良く使います。

アクセスの度にロックするのもなーと思ってシングルトンのマルチスレッド対応について色々と調べていたら、このパターン（遅延初期化）の実装には注意が必要みたいですね。

https://www.jpcert.or.jp/java-rules/lck10-j.html



- [C# クラス実装例](#c-クラス実装例)
- [Unity の場合……](#unity-の場合)
    - [非同期 `get` `set` 問題](#非同期-get-set-問題)
    - [対処方法](#対処方法)
        - [~~TODO: これでも良さそう~~](#todo-これでも良さそう)
- [参考資料](#参考資料)
    - [`ArrayPool.Shared`](#arraypoolshared)
    - [`.Value` が付いても良いなら `Lazy<T>`](#value-が付いても良いなら-lazyt)
        - [C# に `AsyncLazy<T>` は存在しない](#c-に-asynclazyt-は存在しない)
    - [`async set` / `async get`](#async-set--async-get)
    - [`async void` で良いのか問題](#async-void-で良いのか問題)
    - [`volatile` は Java 限定の話？](#volatile-は-java-限定の話)
    - [C#: `Volatile` クラス](#c-volatile-クラス)
    - [元のスレッドには戻れる／メインスレッドには戻れない](#元のスレッドには戻れるメインスレッドには戻れない)
- [おわりに](#おわりに)





# C# クラス実装例

上記の資料ほぼそのままですがピュア C# だとこんな感じですかね。ロックではなく `Interlocked.CompareExchange` を使ってます。

<sup>`lock` の方が早いという説がありますが、高速化のためのダブルチェックなので気にしなくて良いでしょう。</sup>


<!--
> `volatile` は無くても大丈夫という説も調べると見つかりますが、最適化が可能な場合は書き込み／読み込み順序を並び替えることを許可しないというモデファイアなので、一回しか書き込まれないフィールドにとってネガティブな要素は無いので付けておきましょう。
-->


```csharp
// volatile を付けると、変数へのアクセス順序が入れ替わり得る最適化（シングルスレッドなら問題ない）を抑制できる
volatile static MySingleton? _instance;
public static MySingleton Instance
{
    get
    {
        if (_instance == null)
        {
            // 冒頭のヌルチェックが無いとアクセスの度にロック処理を行うことになってしまう
            Interlocked.CompareExchange(ref _instance, FactoryMethod(), null);
        }
        return _instance;
    }
}
```


<p><details lang="ja" --open><summary>Interlocked でファクトリーメソッドを使う理由</summary>

最初のヌルチェックを複数のスレッドがパスした場合、Interlocked で new() した後に初期化するロジックだと別のスレッドは初期化が済んでいないインスタンスを返す可能性がある。

```csharp
if (Interlocked.CompareExchange(ref _instance, new(), null) == null)
{
    // ココで初期化を行う（テンプレとしてよく見るコード？ 当初はなんとなく使ってしまっていた）
}

// new() の後の if 文に入るスレッドは最初の一つのみで、他のスレッドは初期化を待たず return してしまう
return _instance;
```

<!------- End of Details JA Tag -------></details></p>





# Unity の場合……

`UnityEngine.Object` の場合はダブルチェックの２回目をメインスレッド上で実行する方法が必要です。

そして Unity 2023 で遂に！ 待望の！ `Awaitable` が実装されました。

https://docs.unity3d.com/2023.1/Documentation/ScriptReference/Awaitable.MainThreadAsync.html

--

が！！！

```csharp
public static MySingleton Instance
{
    async get  // 👈👈👈
    {
        if (_instance == null)
        {
            await Awaitable.MainThreadAsync();

            if (_instance == null)
            {
                _instance = MySingleton.Create();
            }
        }
        return _instance;
    }
}
```


`async get` なんていう非同期ゲッターは実装出来ません！！

で、👇 👇 👇


```csharp
// volatile!!
volatile static MySingleton? _instance;
public static MySingleton Instance
{
    get
    {
        if (_instance == null)
        {
            InitializeInstance();
        }
        return _instance;
    }
}

// ゲッターはイベントと捉えて async void を良しとすべき？？ つーかこれじゃ多分想定通りに動かないよね？？
static async void InitializeInstance()
{
    await Awaitable.MainThreadAsync();
    if (_instance == null)
    {
        _instance = MySingleton.Create();
    }
}
```


書いてみたけど多分ダメでしょうね。。。





## 非同期 `get` `set` 問題

上記のコメントの通り多分まともに動かないですね。メインスレッドに処理が移るその合間にゲッターの `return` に到達する可能性があります。

（しないケースの方が多いかもしれませんがそれは一番厄介なパターンのバグですね）

<p><details lang="ja" --open><summary>簡易的なテストコード</summary>

実行環境： https://dotnetfiddle.net/

```csharp
using System;
using System.Threading.Tasks;
					
public class Program
{
	public static void Main()
	{
		Test();
		Console.WriteLine("=== 完 ===");
	}
	
	static async void Test()
	{
		await Task.Delay(1000);
		Console.WriteLine("見えてるーー？");
	}
}
```

<!------- End of Details JA Tag -------></details></p>


> Awaitable の実装次第ですが、やってることが同期コンテキストと同様の処理なら性質上ミリ秒単位の遅延は絶対に入ります。




## 対処方法


対処するには `UnitySynchronizationContext` を参考に同期的に処理されるようにする必要があります。

https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/UnitySynchronizationContext.cs#L44


--

~~`Lazy<T>` もありやなしや。~~ ナシ。参考資料を参照。

~~https://learn.microsoft.com/ja-jp/dotnet/api/system.lazy-1~~


--

というか普通に Unity の同期コンテキストに `Send` した方が良いでしょう。

（なんで素直に `RunOnMainThread` を実装してくれなかったんだ。。。色んなライブラリがそれぞれ実装するぐらい Unity には必要なものだろ。。。）

https://qiita.com/sator_imaging/items/48296a686e4bbff8676c#todo-runonmainthread-%E3%82%82%E3%81%A9%E3%81%86%E3%81%9B%E3%81%AA%E3%82%89%E6%AC%B2%E3%81%97%E3%81%84


- https://qiita.com/odayushin/items/fd729c2752121c84e0e6
- https://github.com/googlesamples/unity-jar-resolver/blob/master/source/VersionHandlerImpl/src/RunOnMainThread.cs
- https://github.com/firebase/firebase-unity-sdk/blob/main/app/platform/Dispatcher.cs#L23




### ~~TODO: これでも良さそう~~

```csharp
Awaitable.MainThreadAsync().GetAwaiter().GetResult();
```

ダメでした。詳細は以下に。

https://qiita.com/sator_imaging/items/76fdd84373fca79269e2



--

なぜ await することで実行スレッドが変わるのかというと、continuation をメインスレッドの同期コンテキストに Post してるから。要するに上記のメインスレッドディスパッチャーと同じことをしている。

https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/Awaitable.Threading.cs#L31




# 参考資料

備忘録。

## `ArrayPool.Shared`

静的フィールドの初期化時にインスタンスを挿してますね。

https://github.com/dotnet/corert/blob/master/src/System.Private.CoreLib/shared/System/Buffers/ArrayPool.cs#L23


## `.Value` が付いても良いなら `Lazy<T>`

`Lazy<T>` という手も。ただし、、、

https://learn.microsoft.com/ja-jp/dotnet/api/system.lazy-1



### C# に `AsyncLazy<T>` は存在しない

実装の予定もないようです。

前出のゲッターの例の通り、非同期処理というのはメソッドに `async` を付ければ良いという訳ではないので `Lazy<T>(async () => { ... })` としても何の意味もありません。普通にバグるので注意。

`AsyncLazy<T>` の実装方法は以下の２０１１年（！）の記事を参照。

https://devblogs.microsoft.com/pfxteam/asynclazyt/

https://blog.okazuki.jp/entry/2013/07/23/221918



## `async set` / `async get`

2020 年の投稿で Unanswered。実装の予定は無さそうな？

https://github.com/dotnet/csharplang/discussions/3227



## `async void` で良いのか問題

https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming

（良くないです）



## `volatile` は Java 限定の話？

冒頭の JPCERT の記事は Java を例にしています。C# の `volatile` については以下の通り。

https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/volatile

> 注意
>
> マルチプロセッサ システムでの揮発性読み取り操作では、任意のプロセッサがそのメモリ位置に書き込んだ最新の値を取得することが保証されません。 同様に、揮発性書き込み操作では、書き込まれた値が他のプロセッサにすぐに表示されることが保証されません。

（volatile はロックなしで何かを保証するわけじゃないよ、と読むべきか。難しい日本語ですね。。。）


ちなみに Java の volatile も条件を満たす場合に限るようです。

https://www.jpcert.or.jp/java-rules/vna00-j.html



## C#: `Volatile` クラス

もある。

https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.volatile



## 元のスレッドには戻れる／メインスレッドには戻れない

C# のタスク関連の処理は「同じスレッドで実行する／元のスレッドに戻る」系のパラメーターは多い。

https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcontinuationoptions

ただタスク系の処理は何も指定しないとメインスレッド以外を使うのがデフォなので、処理が重なるとすぐに「元のスレッド＝メインスレッド」が崩れてしまう。（実行スレッドの指定が不可能なタスク処理もある？）

外部のライブラリが内部でタスク関連の処理を行っていると、パラメーターをどう弄ってももうメインスレッドには戻れない状態になってることもある。別スレッドで回ってるタスクのコールバックとか。

なので Unity にとっては元のスレッドとかどうでも良くて特定のタイミングで「メインスレッドに戻る」方法が必要。で、`Awaitable.MainThreadAsync()` が実装された訳だが。



# おわりに

<p><details lang="ja" --open><summary>長めの駄文</summary>

`await Awaitable.MainThreadAsync();` が Unity 特有の事情からスゴイ便利に使えるのは分かるけど、`setter` `getter` での利用は想定してない？

呼び出し側にメインスレッドを強制すれば良いっちゃ良いんだけど RunOnMainThread でも良いよね。呼び出し側も async/await コンテキスト内から呼べるとは限らないんだし。

「待機可能なメインスレッド／バックグラウンドスレッド」を待つと、なんとその後は実行スレッドが代わります！ なんて変な構文を入れて RunOnMainThread は入れない理由なんてなくない？ 素直に RunOnMainThread の公式対応マジで頼む。


長いけど `UnitySynchronizationContext.Default` でも良い。`SynchronizationContext.Current` は「メインスレッドで実行するための同期コンテキストを得るためにメインスレッドで実行しなきゃならない」からダメなんだ。

API として色々おかしなことになるけど `await Awaitable.MainThreadAsync(Action)` のオーバーロードの追加もあり。

（`MoveToMainThreadAsync()` `LeaveFromMainThreadAsync()` とかそういう名前じゃない感じ、もともとは RunOnMainThread 相当だった疑惑がある。メインスレッドでアクションを実行してその完了を待つなら自然だし await しなければ待たずに投げっぱなしにも出来るから便利。await MainThreadAsync() じゃ漠然とし過ぎててサーマルスロットリング対策で処理が落ち着くのを待つ？ とか捉えようが色々とある。まさか実行スレッドを await で変えるとか思わんテクニックとしては面白いけど）

<!------- End of Details JA Tag -------></details></p>


--

Awaitable ちょっと謎ですね。2023 LTS でしれっと変わるか？

以上です。お疲れ様でした。