1. はじめに
スレッドをアイスクリームの列に並んでいる人たちと想像してみよう。静かに並んでいる人もいれば、騒いで「電車に乗り遅れそうだ、先にして!」と主張する人もいる。店員はみんなの声を聞くけど、時々列を外して先に対応することがある、例えば泣いている子どもを連れたお母さんとか。スレッドの優先度は大体そんな感じだよ。
C#(正確には .NET)では、各スレッドに優先度があって、それは他のスレッドと比べてどれくらい「重要」かを OS に伝えるためのヒントになる。これは OS に対する絶対的な命令ではなく(最高優先度のスレッドが常に全部の CPU リソースを独占する保証はない)、通常は高い優先度のスレッドがより多くの CPU 時間を得やすい、という程度の扱いだ。
どんな場面で本当に必要か?
- ユーザーインターフェース: 画面の更新は、低優先度の重い計算に妨げられないようにするべき。
- ゲーム: フレームのレンダリングは、バックグラウンドでのマップ生成よりも重要。
- リアルタイム性が重要な処理: 機器制御や信号処理など。
2. スレッドの優先度: 仕組み
C# ではスレッドの優先度を扱うのは簡単で、Thread オブジェクトに Priority プロパティがある。これは列挙型 ThreadPriority の値を取る:
| 値 | 説明 |
|---|---|
|
最も低い優先度 |
|
通常より低い |
|
通常(デフォルト) |
|
通常より高い |
|
最も高い優先度 |
コード例:
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread lowPriorityThread = new Thread(PrintLowPriority);
Thread highPriorityThread = new Thread(PrintHighPriority);
lowPriorityThread.Priority = ThreadPriority.Lowest;
highPriorityThread.Priority = ThreadPriority.Highest;
lowPriorityThread.Start();
highPriorityThread.Start();
}
static void PrintLowPriority()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("低い優先度: " + i);
Thread.Sleep(10); // 違いを見やすくするために少し待つ
}
}
static void PrintHighPriority()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("高い優先度: " + i);
Thread.Sleep(10);
}
}
}
メモ
実際には、特に現代のマルチコアシステムやマネージドな .NET 環境では、優先度は OS への「提案」に過ぎない。OS(と .NET)は高優先度のスレッドにより多く時間を割こうとするが、厳密な保証はない。例えば、もしあなたのスレッドが Highest で CPU を独占し始めたら、UI が固まったり他のスレッドに影響が出たりする可能性があるよ。
3. 優先度の変更
スレッドと優先度
graph LR
A[スレッド 1 - Lowest] -->|少ないプロセッサ時間| OS(オペレーティングシステム)
B[スレッド 2 - Normal] --> OS
C[スレッド 3 - Highest] -->|多いプロセッサ時間| OS
OS --> CPU(CPU(プロセッサ))
なぜ優先度を変えるのか?
全部を常に Highest にすればいいんじゃないか、と思うかもしれないけど、それは良いアイディアじゃない。店で全員が同時に「先にしてくれ!」と言い出したらどうなるか想像してみて。
優先度は正当な理由がある場合にだけ変更しよう:
- ログを書き込むバックグラウンドスレッド — BelowNormal や Lowest にしてもよい。
- ユーザー入力を扱うスレッド — AboveNormal。
- CPU 集約で時間的にクリティカルでない処理 — 低い優先度。
ちょっとしたコツ: アプリが重くなってきたら、「図々しい」高優先度スレッドがないかチェックしてみよう。そいつらが他を邪魔しているかもしれない。
4. .NET におけるスレッドの種類(カテゴリ)
C# では伝統的に 2 種類のスレッドを区別する: foreground と background。ちょっと紛らわしいけど、"background" は単に「裏で動くもの」という意味だけじゃない。以下で説明するよ。
Foreground スレッド(メイン系)
- 通常、あなたが作るスレッドはすべて foreground になる。
- プロセス内に foreground スレッドが一つでも生きている限り、アプリケーションは終了しない。
- 例: プログラムのメインスレッド (Main)、および new Thread() で生成されたスレッド(特別な設定をしない場合)。
Background スレッド(補助系)
- すべての foreground スレッドが終了して、残っているのが background スレッドだけだと、プロセスは終了し、これらのスレッドは予告なしに中断される。
- ログ記録やメトリクス送信などの二次的なタスクに使われる。
- スレッドをバックグラウンドにするには、IsBackground を true にする:
Thread t = new Thread(SomeMethod);
t.IsBackground = true;
t.Start();
違いのデモ
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread backgroundThread = new Thread(() =>
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Background スレッドが動作中... " + i);
Thread.Sleep(500);
}
Console.WriteLine("Background スレッドは完了しました。");
});
backgroundThread.IsBackground = true; // スレッドをバックグラウンドにする!
backgroundThread.Start();
Console.WriteLine("Main は 1 秒後に終了します。");
Thread.Sleep(1000); // 1 秒待つ
Console.WriteLine("Main は終了しました。Background スレッドはどうなるかな?");
}
}
何が見えるか?
Main が終了すると、Background スレッドは途中で打ち切られることがある。アプリの終了を妨げたくない処理にこれは便利。
5. スレッドの種類
ThreadPool(スレッドプール)
- ThreadPool はスレッドのプールを管理する仕組みで、スレッドを過剰に作らないようにする。
- たくさんあって短時間で終わるタスクに向いている: 同時リクエスト処理や非同期操作など。
- プールから取得したスレッドは常に background — アプリの終了を妨げない。
例: スレッドプールでコードを実行する
using System;
using System.Threading;
class Program
{
static void Main()
{
ThreadPool.QueueUserWorkItem(DoWorkInThreadPool, "バックグラウンドのタスク");
Console.WriteLine("Main は終了処理を行います。");
Thread.Sleep(500);
}
static void DoWorkInThreadPool(object? state)
{
Console.WriteLine("プールのスレッド: " + state);
Thread.Sleep(1000); // ちょっとスリープしてみる
Console.WriteLine("プールのスレッドは終了しました!");
}
}
注意点:
もし Main が先に終わってしまうと、プールのスレッドは中断されうる。完了を保証したいなら foreground スレッドを使うか、明示的に待つ必要がある。
並行処理の進化: Task と async/await
現代のアプリでは手動でスレッドを作る (Thread) ことは少なく、通常は Task や非同期メソッドを使う。ここで知っておくべき点:
- プールのスレッドや Task は background スレッドで動作することが多い。
- ほとんどの場合、優先度を変える必要はない — 常識とベストプラクティスに従おう。
6. 役に立つ細かい点
スレッドのプロパティと挙動
| プロパティ | Foreground Thread | Background Thread | ThreadPool Thread |
|---|---|---|---|
|
false がデフォルト | true | true |
| アプリケーションの終了 | 少なくとも 1 つの foreground スレッドが生きている限りアプリは終了しない | すべての foreground が終了するとアプリは終了する | Main が終了するとアプリは終了する |
| 制御 | 完全な制御が可能 | 完全な制御が可能 | 直接の制御はできない |
| 利用される場面 | 長時間で重要なタスク(例: DB サーバなど) | 非クリティカルなタスク、バックグラウンド処理、ログ記録 | 短時間のタスク、Task、非同期処理 |
| 優先度 | 設定可能 | 設定可能 | 変更は推奨されない |
気づきにくいポイントと注意
- 優先度の変更は通常のスレッド(Thread)に対してのみ可能で、プール由来のタスク(Task, ThreadPool)には適用できない。
- プールのスレッドは常に Normal 優先度で、変更できない。
- Task と async/await はよりモダンなアプローチで、優先度やバックグラウンド実行の多くの問題を内部に隠蔽してくれる。
7. 優先度やスレッドタイプに関する典型的なミス
ミスその1: 優先度の乱用。
なんとなく全てを Highest にしたり、全部を Lowest にしたりするのは意味がない。実行バランスを崩してアプリの応答性を下げることがある。
ミスその2: background スレッドの暗黙の中断。
重要な処理(例えばデータ保存)を background スレッドでやっていて、それが完了するのを待たずにアプリを閉じるとデータを失う可能性がある。background スレッドはプロセス終了時に自動で中断される。
ミスその3: 優先度に対する過度な期待。
スレッドの優先度(Priority)は OS への「提案」に過ぎず、必ず先に実行されるという保証ではない。
GO TO FULL VERSION