C#の非同期処理時のCurrentCultureは.NET/.NET Frameworkのバージョンで挙動が変わる

## はじめに

こちらの記事を拝見して、自分の環境で試したら色々と気になる点があったのでまとめました。

https://qiita.com/kansou/items/d7986798f658ffa0ce58

## 結論
- .NET Framework 4.5.2 までの環境では、非同期処理時のCurrentCultureは都度リセットされる
- .NET Framework 4.6 以降の環境では、非同期処理時のCurrentCultureは起動元のスレッドのものを引き継ぐ

## 公式ドキュメント確認

https://learn.microsoft.com/ja-jp/dotnet/fundamentals/runtime-libraries/system-globalization-cultureinfo#culture-and-task-based-asynchronous-operations
> .NET Framework 4.6 以降のバージョンを対象とするアプリの場合、カルチャは非同期操作のコンテキストの一部です。 つまり、非同期操作は、起動元のスレッドの CurrentCulture と CurrentUICulture プロパティの値を既定で継承します。 現在のカルチャまたは現在の UI カルチャがシステム カルチャと異なる場合は、現在のカルチャがスレッドの境界を越えて、非同期操作を実行しているスレッド プール スレッドの現在のカルチャになります。

https://learn.microsoft.com/ja-jp/dotnet/framework/migration-guide/retargeting/4.6.x#currentculture-and-currentuiculture-flow-across-tasks
> .NET Framework 4.6 より、非同期操作全体をフローする、スレッドの System.Threading.ExecutionContext に System.Globalization.CultureInfo.CurrentCulture と System.Globalization.CultureInfo.CurrentUICulture が格納されます。その結果、System.Globalization.CultureInfo.CurrentCulture または System.Globalization.CultureInfo.CurrentUICulture に対する変更は、後で非同期実行されるタスクで反映されます。 これは、すべての非同期タスクで System.Globalization.CultureInfo.CurrentCulture と System.Globalization.CultureInfo.CurrentUICulture がリセットされていた、以前のバージョンの .NET Framework の動作とは異なります。

## 内部実装確認
System.Threading.Thread.CurrentCultureは、単にSystem.Globalization.CultureInfo.CurrentCultureを参照しています。

https://github.com/dotnet/runtime/blob/release/9.0/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs#L294

## 動作確認
テストコードは、前述した記事のものを一部参考にさせていただいています。
```C#:テストコード
static async Task Main(string[] args)
{
    Console.WriteLine($"Main thread default {DateTime.Today}");

    CultureInfo originalCulture = new CultureInfo("ja-JP");
    originalCulture.DateTimeFormat.Calendar = new JapaneseCalendar();
    originalCulture.DateTimeFormat.ShortDatePattern = "ggy年M月d日";
    Thread.CurrentThread.CurrentCulture = originalCulture;

    Console.WriteLine($"Main thread after custom {DateTime.Today}");

    await Task.Run(() => {
        Console.WriteLine($"Task.Run after custom {DateTime.Today}");
    });

    var thread = new Thread(() => {
        Console.WriteLine($"Thread.Start after custom {DateTime.Today}");
    });
    thread.Start();
    thread.Join();
}
```

```.NET Framework 4.5.2 環境でのテスト結果
Main thread default 2025/01/08 0:00:00
Main thread after custom 令和7年1月8日 0:00:00
Task.Run after custom 2025/01/08 0:00:00
Thread.Start after custom 2025/01/08 0:00:00
```
```.NET Framework 4.6 環境でのテスト結果
Main thread default 2025/01/08 0:00:00
Main thread after custom 令和7年1月8日 0:00:00
Task.Run after custom 令和7年1月8日 0:00:00
Thread.Start after custom 令和7年1月8日 0:00:00
```
```.NET 9.0 環境でのテスト結果
Main thread default 2025/01/08 0:00:00
Main thread after custom 令和7年1月8日 0:00:00
Task.Run after custom 令和7年1月8日 0:00:00
Thread.Start after custom 令和7年1月8日 0:00:00
```

## 調べて追加で気になったこと

.NET Framework 4.6 以降の環境で、ExecutionContextのキャプチャを行わない従来の動作を意図的に行いたい場合、どうするか。

#### 解決法
1. ThreadクラスのUnsafeStartメソッドを使ってスレッドをスタートする(※NET 6.0 以降)
2. ExecutionContext.SuppressFlowメソッドを使用する

#### 参考リンク

https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread.unsafestart?view=net-9.0

https://github.com/dotnet/runtime/issues/80000#issuecomment-1453558110