概要
ThreadとTaskの違いを説明する。一般的なサイトではTaskを使用を推奨しているが、機器制御系のソフトウェアではどの様に使用すべきかを検討して、状況別に分けた使用方法を説明する。
また、スレッドの振る舞、注意点が纏めらていない様に思われる為、制御系の観点からスレッドを使用する上での重要な事柄を纏める
Threadについて
Thread クラスは、C# で実際のOSシステムレベルのスレッドを作成します。Thread クラスで作成されたスレッドは、スタックのメモリなどのリソースを使用し、コンテキストの CPU オーバーヘッドはあるスレッドから別のスレッドに切り替わります。Thread クラスは、Abort() 関数、Suspend() 関数、Resume() 関数などの高度な制御を提供します。スタックサイズなどのスレッドレベルの属性を指定することもできます。
引用 :https://www.delftstack.com/ja/howto/csharp/thread-vs-task-in-csharp/
参考:https://ufcpp.net/study/csharp/misc_task.html#task
Taskについて
Task クラスは、C# で非同期のシステムレベルのタスクを作成します。タスクスケジューラは、Task クラスで作成されたタスクを実行します。デフォルトのスケジューラーは、スレッドプール内でタスクを実行します。Thread クラスで作成されたスレッドとは異なり、Task クラスで作成されたタスクは、追加のメモリや CPU リソースを必要としません。Task クラスを使用して、スタックサイズなどのスレッドレベルの属性を指定することはできません。Task クラスはスレッドプールで実行されるため、長時間実行されるタスクはスレッドプールをいっぱいにする可能性があり、新しいタスクは前のタスクの実行が完了するのを待つことになります。
引用 :https://www.delftstack.com/ja/howto/csharp/thread-vs-task-in-csharp/
参考:https://ufcpp.net/study/csharp/misc_task.html#task
Taskのスレッドスプールについて
Taskのスレッドスプールは、デフォルトで4個まで同時に実行ができる。
各スレッドが終わらない場合は、次のTaskは実行を待つことになる。
実験サイト:https://oita.oika.me/2016/02/18/task-and-threadpool/
スレッドスプールの参考:https://www.nowonbun.com/39.html
結論
長時間の動作は、Threadを使用する。
(スレッドの起動は予め実施、プログラム終了と同時に破棄する作りにする 但し、メモリや動作速度の関係で動作中にスレッドを終了させるかは、検討する)
短時間で終了する処理についてはTaskを使用する。
例えば
IOを常に監視する Thread
検査を実行するステートマシーン Thread
ボタンイベントなどで、起動する短時間の処理 Task
System.Threading.Timerについて
Timerはスレッドスプールを使用して管理している為、TaskとTimerでスレッドスプールが満杯になる場合は、他のTimerが正常に実行されないことがある。
スリープについて
Thread.Sleepは、現在のスレッドを指定された時間だけブロック(休止)させる。
UIスレッドで使用する場合は、UIがフリーズするので注意が必要
精度は、通常15.6ミリ秒(Windows 7以前)または1ミリ秒(Windows 8以降)。これは、Windowsのタイマー解像度に関連している。したがって、15.6ミリ秒未満のスリープを指定しても、実際には15.6ミリ秒のスリープが行われる可能性がある。
Task.Delayは非同期で処理を一時的に停止する為、Sleepメソッドと違い、UIのフリーズ状態にはならない。精度は、Thread.Sleepと同様
Threadを使用する時は、UIの事は考えない為、待ちたい場合は、Sleepを使用しても問題ない
UIをフリーズさせたくない目的で使用する場合や、TaskでのWait方法は、Task.Delayを使用する
以下のサイトで使用方法の実験を行っている
UIでフリーズが発生する組み方と、問題ない組み方が分かりやすく記載されている
https://www.hanachiru-blog.com/entry/2020/05/26/120000
Threadの無限ループ(長期動作)について
While(条件){処理}のような形で、ある条件でループさせてThreadを使用する場合がある。
マイコンでは、状態により動作するステートマシーンを作成した場合、全力でステートマシーンをループするが、PCアプリケーションで同じことを実施するとスレッドが処理を専有するため、他の処理やイベントハンドリングがブロックされる可能性がある。これにより、アプリケーション全体の応答性が低下する可能性がある。
その為、PCアプリケーションのステートマシーンは、Sleep(休止)を入れて他のスレッドの割り込みができるようにしておくことが必須となる。 つまりは、ステートマシーンの最大反応速度は、15msec程度とか考えて設計をする必要がある。
Thred Task 内での Thread Taskの生成
基本的には、実施しない方が良いと思われる。 SleepやDelayが、どのスレッドまで影響するか検討するのがややこしくなる為。
Thread、TaskからのUIへのアクセス
WindowsForm WPF共に他スレッドからUIへアクセスるには処理の移譲(delegate)を実施する必要がある
this.Invoke(new Action(() =>
{
textBox1.Text = "Test";
}));
this.Dispatcher.Invoke((Action)(() =>
{
textBox1.Text = "Test";
}));
注意点としては、「処理を移譲する」とあるが、UIに処理を移譲して、処理が完了するまで待って、次の処理が実行される。 その為、UIの更新処理が忙しい場合は、スレッドの動作に影響が出る為、注意が必要となる
スレッドセーフ処理
複数のスレッドからデータに対してはアクセスする場合は、スレッドセーフに作成しないと整合性が取れない場合があり、Exceptionやバグの原因となる。以下にスレッドセーフに使用する為の方法を記載する
lock
lock にオブジェクトを渡すと、そのオブジェクトを取得したスレッドだけが、そのコードブロックに入れるようになります。そして、コードブロックから出るときに、そのオブジェクトを解放する。
static int val = 0;
static Object lockObj = new Object();
/// 複数スレッドから呼び出しが行われる
static void SampleData() {
for (var m = 0; m < 100000; m++) {
lock (lockObj) {
val++;
}
}
}
注意としては、
互いのスレッドがlock解放待ち状態になるとデットロックの原因となる
lock範囲が広い場合は、待ち時間が長くなるので最小範囲とすること
BlockingCollection
lockのような排他制御を実施すれば、複数スレッドから同時に触れないようする事が可能だが、C#のBlockingCollectionクラスは、同時にAddしたりAdd中にGetしたりするのを防ぐ仕組みある。
自前で実装しても問題ないが、C#ではこれを簡単に実現するためのBlockingCollectionというクラスを使用する。
参考:https://light11.hatenadiary.com/entry/2021/06/10/204532
注意点
BlockingCollectionはある要素を取得したらその要素を削除するようなコレクションを想定して作られている。またインデックス指定はできず、TryTake()により「次の」要素が取得できるだけ。
ConcurrentBagは順序性が不定、ConcurrentQueueやConcurrentStackを渡すとそのBlockingCollectonはそれぞれQueueやStackとして振舞う
調査時の環境
.NET 6 をベースに調査
VisualStudio 2022