この記事では、C#におけるスレッドとThreadクラスを利用したマルチスレッド処理の方法について説明します。 まず前提知識として、スレッドの概要についてまとめると以下の通りです。
- CLRまたはWindows環境では、実行するプログラムごとにプロセスと呼ばれる仮想アドレス空間が作成されます。
- 各プロセスは独自のスレッドを持ち、このスレッドはプロセスの全てデータにアクセスできます。
- プロセスの実行では、必ず
Main
メソッドから実行を開始するメインスレッドが存在します。 - このメインスレッドの実行中に、1つ以上のスレッドを作成することができます。
- これらのスレッドは、同じ実行可能ファイル内のコードや、同じプロセス内の他のDLLで定義されたコードを実行できます。
- Windowsでは、プロセスにスレッドが存在しない場合、そのプロセスは終了します。
- 複数のスレッド(マルチスレッド)を作成することで、マルチタスクを実現できます。複数のタスクが同時に実行されるため、時間を節約できます。
Thread
以下は、メインスレッド内でThreadクラスを使ってスレッドを作成するサンプルプログラムです。 Threadのコンストラクタでは、スレッド化したいメソッド(無名関数でも可)を指定します。 Thread#Startでスレッドを起動し、Thread#Joinでスレッドが終了するまで待機します。
なお、スレッドを引数パラメータありで起動するとき、パラメータを渡される側は object 型となるため、必ず string 型などにキャストしてから利用してください。
using System;
using System.Diagnostics;
using System.Threading;
public class StartThreadExample
{
public static void Main()
{
Thread thread = new Thread(Download);
// スレッドを起動(引数パラメータあり)
thread.Start("https://example.com");
// スレッドが終了するまで待機
thread.Join();
Console.WriteLine("Finished!");
}
// 並列化したい処理をメソッドにする
private static void Download(object? arg)
{
string url = "";
if (arg is not null)
{
url = (string)arg;
}
Console.WriteLine($"[*] Downloading {url}");
}
}
注意点として、スレッドは非常に重い処理を並列で実行するときによく利用されますが、すぐに終わる処理でもなんでもかんでもスレッドにするのは危険です。 ほとんどのOSでは、スレッドの生成と終了は比較的負荷の高い処理です。 プログラムがスレッドを生成すると、OSがそのスレッドに対して「スタック」と呼ばれるメモリ領域(1MB程度)を割り当てます。 そのため、もし仮に1000個のスレッドを作成してしまうとスタックだけで約1GBのメモリを消費してしまいます。
また、OSにはシステム全体で起動できるスレッドの上限が設定されています。 OS側では、実行可能なスレッドを定期的に切り替えてすべてのスレッドが処理を進められるようにスレッド切り替え(コンテキストスイッチング)が行われており、スレッドの数が増えるほどこの切り替え処理の全体にかかる時間が長くなり、システム全体への負荷が重くなります。
この問題を解決するために、.NETには「スレッドプール」という機能があります。 これは、Taskクラスを使ってアクセスできる機能で、スレッドをプールしておき、スレッドを再利用しながら、タスクを処理していく仕組みです。 これにより、システム全体のスレッド数をハードウェアスレッド数(CPUが実際に同時に処理できる命令の数)に近づけることができ、スレッド切り替えのコストを最小限にすることができます。
そのため、簡単な処理(例えばWebページの取得など)であれば、スレッドプールを使用するTask
を利用した並列プログラミングをすることが推奨されます。
以上です。