非同期や並列プログラミングの複雑さを抽象化するために多数のライブラリが存在するが、それでも開発者は、時々、下位のスレッド処理ロジックへの落とし込みが必要になる。.NET6シリーズのAPIの変更に続いて、マルチスレッドのいくつかの新しい効果的な方法を見ていこう。
Async Parallel.ForEach
Parallelクラス が作成された時に、C#には非同期プログラミングの言語サポートがなかった。.NET 1.1 からIAsyncResultパターンがあったが、それほど使われておらず、コードの大部分は同期的に実行されるように設計されていた。
async/awaitを使用した非同期コードが注目されるようになり、このことが問題になった。現在、 Parallel.ForEach
操作を開始し、結果を非同期に待つビルトインサポートはない。GSPPは次のように述べた。
私はStack Overflowで非常に活発に活動していて、私は人々がいつも非同期プログラミングの言語サポートを必要としているのを見ています。人々は、すべてのアイテムを同時に並行して開始し、WhenAllを使うような非常に悪い回避策を使用します。そのため、10000 HTTP呼び出しを開始し、どうしてそんなにパフォーマンスが悪いのかと考えます。または、アイテムをバッチで実行します。バッチは1つずつ完了していき、効果的なDOP[並列度]が減少するため、これははるかに遅くなる可能性があります。または、タスクのコレクションを使った非常に厄介なループコードや変な待機スキームを書きます。
この懸念に対処するために、一連の Parallel.ForEachAsync関数 が作成された。これらの関数は、IEnumerable、または、IAsyncEnumerableを引数にする。ParallelOptionsとCancellationTokenも引数となる。
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, CancellationToken cancellationToken, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, CancellationToken cancellationToken, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TSource, CancellationToken, ValueTask> body)
Parallel.ForEachAsync
は、 Task
の代わりにValueTask
が使われる珍しいケースの一つだ。ここでの基本的な考え方は、値がタイトなループで処理されるため、完全な Task
オブジェクトを作成する余分なオーバーヘッドは正当化されない。Stephen Toub氏は次のように書いた。
私は、Funcは
Task
よりもValueTask
を返すべきだと思います。ValueTask
の主な懸念は、ValueTaskが間違って消費されることです。しかし、ここで消費するのは私たちが実装しているメソッドであり、私たちはそれが正しいことをするようにするだけです;)ValueTask
を返すことは、もっと適応しています。:Task
はValueTask
(まだTask
にラッピングされていない場合)に非常に簡単に変換できますが、Task
にValueTask
を変換するには割り当てが必要です。
全体的にこの機能は好評だったが、論争の1つのポイントは、開発者が並列度の値を提供する必要があるかどうかだ。これは、並列操作に割り当てられるスレッドの数に大体相当する。
ほとんどの開発者は、作業負荷の理想的な並列度を知らず、彼らにそれを提供することを期待するのは逆効果だろうと決められた。デフォルトで選択されているのは Environment.ProcessorCount
だ。
Thread.ManagedThreadId 非推奨
Environment.CurrentManagedThreadId
は、Thread.ManagedThreadId
プロパティのより効率的な代替手段として.NET4.5で導入された。しかし、このことはドキュメントでは伝えられず、開発者は Thread.ManagedThreadId
を引き続き使用している。
開発者をより良い選択肢に導くために、コード分析の警告が Thread.ManagedThreadId
に追加された。
この警告は、実際には Thread.ManagedThreadId
が非推奨であることを意味し、廃止とは記されていない。今はEnvironment.CurrentManagedThreadId が好まれていても、開発者は、当面はThread.ManagedThreadId
を使用し続けるかもしれない。
Thread.UnsafeStart
スレッドを開始するための新しい関数は、実行コンテキストをキャプチャしていないため、"unsafe"と呼ばれる。David Fowler氏が次のように説明する。
私たちは、 このPR#46181に UnsafeStart を追加しました。デフォルトの実行コンテキストでスレッドプールスレッドとタイマスレッドを遅延的に作成する必要があったためです。UnsafeStartは、現在の実行コンテキストをキャプチャし、スレッドの実行時に復元することを回避します。同様のロジックを使用できるスレッドを作成する場所は他にもあります。
この機能は、既に次のような多数の場所で使用されている。
FileSystemWatcher
(OS X)SocketAsyncEngine
(Unix)CounterGroup
(Tracing API)ThreadPoolTaskScheduler
(タスクがLongRunning
とマークされた時)
ブラウザで実行している場合、 UnsafeStart
は PlatformNotSupportedException
をスローする。
Periodic Timer
PeriodicTimer
クラスは、もともとAsyncTimer と呼ばれていた。これは、非同期コンテキストで使用するように設計されているためだ。以下の例でわかるように、タイマの進む音の間でawaitを使用する必要がある。
var second = TimeSpan.FromSeconds(1);
using var timer = new AsyncTimer(second);
while (await timer.WaitForNextTickAsync())
{
Console.WriteLine($"Tick {DateTime.Now}")
}
David Fowler氏が、PeriodicTimerの設計上の利点を説明する。
このAPIは、繰り返し起動するタイマにのみ意味があり、一度だけ起動するタイマはTaskベースになるでしょう。(このために、既にTask.Delayがあります。)
ユーザーコードの実行中にタイマは時停止され、終了すると次の期間を再開します。
タイマは、列挙を停止するために提供されたCancellationTokenを使用して停止できます。
実行コンテキストはキャプチャされません。
WaitForNextTickAsync
呼び出しが現在実行中でも、タイマは、 Stop
または Dispose
を呼び出して、停止することもできる。
.NETには既に他に5つのタイマがあるが、これらのタイマにはこの特定の機能はない。ドキュメントの一部として、使用するタイマを選択するための新しいガイドが計画されている。
トリビア: PeriodicTimer
クラスは、名前空間を別のタイマと共有する最初のタイマになる。以前は、各タイマは別々の名前空間に配置されていた。
System.Timers.Timer
System.Threading.Timer
System.Windows.Forms.Timer
System.Web.UI.Timer
System.Windows.Threading.DispatcherTimer
シリーズの以前のレポートは、以下のリンクを参照しよう。