

コンピュータサイエンスでは、実行スレッドとは、通常オペレーティングシステムの一部であるスケジューラによって独立して管理できるプログラムされた命令の最小シーケンスです。[ 1 ]多くの場合、スレッドはプロセスのコンポーネントです。
同一プロセスの複数のスレッドは、マルチスレッド機能によって並行して実行され、メモリなどのリソースを共有するが、異なるプロセス間ではこれらのリソースは共有されない。特に、同一プロセスのスレッドは、実行可能コード、動的に割り当てられた変数の値、およびスレッドローカルではないグローバル変数の値を、任意の時点で共有する。
スレッドは、1967年にIBMのバッチ処理オペレーティングシステムOS/360で「タスク」という名前で初めて登場しました。OS/360は、ユーザーに3つのOS/360制御システムの構成を提供し、そのうちの1つが可変数のタスクによるマルチプログラミング(MVT)でした。Saltzer(1966)は、「スレッド」という用語をVictor A. Vyssotskyに帰しています。 [ 3 ]
Machのスレッド実装は 1986 年夏に説明されました。[ 4 ] 1987 年にリリースされたOS/2 1.0 はスレッドをサポートしていました。[ 5 ] Digital ResearchのMP/Mオペレーティングシステムは、1979 年に Intel 8080 プロセッサ向けにマルチプログラミング、マルチタスク、プリエンプティブマルチタスクを提供し、1981 年に 8086 プロセッサ向けに提供しました。スレッドを備えた最初のバージョンのWindowsは、1993 年にリリースされたWindows NTでした。
1995年、IEEEはpthreads APIを定義し、さまざまなUnix系オペレーティングシステム間で移植可能なマルチスレッドプログラミングのインターフェースを標準化しました[ 6 ] 。それ以来、pthreadsは、既存のWindows APIの上に標準を実装するpthreads-w32 [ 7 ]などのサードパーティパッケージによってWindowsにも実装されています。
ソフトウェアアプリケーションにおけるスレッドの使用は、CPUがマルチコアを利用し始めた2000年代初頭に一般的になった。パフォーマンス上の利点を得るためにマルチコアを活用したいアプリケーションは、マルチコアを利用するために並行処理を採用する必要があった。[ 8 ]
スケジューリングはカーネルレベルまたはユーザーレベルで実行でき、マルチタスクはプリエンプティブまたは協調的に実行できる。これにより、さまざまな関連概念が生じる。
カーネルレベルでは、プロセスは1 つ以上のカーネル スレッドを含み、メモリやファイル ハンドルなどのプロセスのリソースを共有します。プロセスはリソースの単位であり、スレッドはスケジューリングと実行の単位です。カーネル スケジューリングは通常、一律にプリエンプティブに行われるか、あまり一般的ではありませんが協調的に行われます。ユーザー レベルでは、ランタイム システムなどのプロセスが、複数の実行スレッドをスケジュールできます。Erlang のように、これらのスレッドがデータを共有しない場合は、通常、同様にプロセスと呼ばれます[ 9 ] 。一方、データを共有する場合は、特にプリエンプティブにスケジュールされている場合は、通常、 (ユーザー)スレッドと呼ばれます。協調的にスケジュールされたユーザー スレッドはファイバーとして知られています。異なるプロセスは、ユーザー スレッドを異なる方法でスケジュールできます。ユーザー スレッドは、さまざまな方法(1 対 1、多対 1、多対多)でカーネル スレッドによって実行できます。軽量プロセスという用語は、さまざまな意味でユーザー スレッドまたはユーザー スレッドをカーネル スレッドにスケジュールするためのカーネル メカニズムを指します。
プロセスはカーネルスケジューリングの重量単位であり、プロセスの作成、破棄、切り替えは比較的コストがかかります。プロセスはオペレーティングシステムによって割り当てられたリソースを所有します。リソースには、メモリ(コードとデータの両方用)、ファイルハンドル、ソケット、デバイスハンドル、ウィンドウ、およびプロセス制御ブロックが含まれます。プロセスはプロセス分離によって分離されており、ファイルハンドルや共有メモリセグメントの継承、または同じファイルを共有方式でマッピングするなどの明示的な方法を除き、アドレス空間やファイルリソースを共有しません(プロセス間通信を参照)。プロセスの作成または破棄は、リソースの取得または解放が必要なため、比較的コストがかかります。プロセスは通常プリエンプティブにマルチタスク化され、プロセス切り替えは、コンテキスト切り替えの基本コストに加えて、キャッシュフラッシュなどの問題により比較的コストがかかります(特に、プロセス切り替えは仮想メモリのアドレス指定を変更し、タグなし変換ルックアサイドバッファ(TLB)の無効化とフラッシュを引き起こします。これは特にx86で顕著です)。
カーネルスレッドは、カーネルスケジューリングの軽量単位です。各プロセスには少なくとも1つのカーネルスレッドが存在します。プロセス内に複数のカーネルスレッドが存在する場合、それらは同じメモリとファイルリソースを共有します。オペレーティングシステムのプロセススケジューラがプリエンプティブである場合、カーネルスレッドはプリエンプティブにマルチタスク化されます。カーネルスレッドは、スタック、プログラムカウンタを含むレジスタのコピー、およびスレッドローカルストレージ(存在する場合)以外のリソースを所有しないため、作成と破棄のコストは比較的低くなっています。スレッド切り替えも比較的コストが低く、コンテキストスイッチ(レジスタとスタックポインタの保存と復元)が必要ですが、仮想メモリを変更しないため、キャッシュに優しく(TLBを有効なまま維持)、効率的です。カーネルは、CPUの各コアに1つ以上のソフトウェアスレッドを割り当てることができ(マルチスレッドのサポート状況に応じて、カーネル自身に複数のソフトウェアスレッドを割り当てることも可能)、ブロックされたスレッドをスワップアウトできます。ただし、カーネルスレッドのスワップアウトには、ユーザスレッドよりもはるかに時間がかかります。
スレッドは、ユーザー空間ライブラリで実装される場合があり、その場合はユーザースレッドと呼ばれます。カーネルはこれらのスレッドを認識しないため、ユーザー空間で管理およびスケジューリングされます。一部の実装では、マルチプロセッサマシン(M:Nモデル)の利点を活用するために、複数のカーネルスレッドを基盤としてユーザースレッドを構築しています。仮想マシンによって実装されるユーザースレッドは、グリーンスレッドとも呼ばれます。
ユーザー スレッドの実装は通常、完全にユーザー空間で行われるため、同一プロセス内のユーザー スレッド間のコンテキスト スイッチは、カーネルとのやり取りを一切必要としないため、非常に効率的です。コンテキスト スイッチは、現在実行中のユーザー スレッドまたはファイバーが使用する CPU レジスタをローカルに保存し、次に実行予定のユーザー スレッドまたはファイバーに必要なレジスタをロードすることで実行できます。スケジューリングはユーザー空間で行われるため、スケジューリング ポリシーをプログラムのワークロードの要件に合わせて容易に調整できます。
しかし、ユーザー スレッド(カーネル スレッドとは対照的に)でブロッキング システム コールを使用すると問題が生じる可能性があります。ユーザー スレッドまたはファイバーがブロッキング システム コールを実行すると、そのシステム コールが戻るまで、プロセス内の他のユーザー スレッドとファイバーは実行できなくなります。この問題の典型的な例は、I/O を実行する場合です。ほとんどのプログラムは、I/O を同期的に実行するように記述されています。I/O 操作が開始されると、システム コールが呼び出され、I/O 操作が完了するまで戻りません。その間、プロセス全体がカーネルによって「ブロック」され、実行できなくなるため、同じプロセス内の他のユーザー スレッドとファイバーは実行できなくなります。
この問題に対する一般的な解決策(特に多くのグリーンスレッド実装で使用されている)は、内部で非ブロッキングI/Oを使用し、I/O操作の進行中に別のユーザースレッドまたはファイバーをスケジュールすることで、プロセス全体ではなく呼び出し元のスレッドをブロックするインターフェースを実装するI/O APIを提供することです。同様の解決策は、他のブロッキングシステムコールにも提供できます。あるいは、同期I/Oやその他のブロッキングシステムコールの使用を避けるようにプログラムを作成することもできます(特に、ラムダ継続やasync/ awaitプリミティブ[ 10 ]を含む非ブロッキングI/Oを使用する)。
ファイバーは、協調的にスケジューリングされるさらに軽量なスケジューリング単位です。実行中のファイバーは、別のファイバーが実行できるように明示的に譲らなければならないため、カーネルスレッドやユーザースレッドよりも実装がはるかに容易です。ファイバーは、同じプロセス内の任意のスレッドで実行するようにスケジュールできます。これにより、アプリケーションは、カーネルスケジューラ(アプリケーションに合わせて調整されていない可能性がある)に依存するのではなく、スケジューリングを自身で管理することでパフォーマンスを向上させることができます。OpenMP 並列プログラミング モデルのいくつかの研究実装では、タスクをファイバー経由で実装しています。[ 11 ] [ 12 ]ファイバーと密接に関連しているのはコルーチンですが、コルーチンは言語レベルの構成要素であるのに対し、ファイバーはシステムレベルの構成要素であるという違いがあります。
スレッドは、従来のマルチタスクオペレーティングシステムのプロセスとはいくつかの点で異なります。
Windows NTやOS/2のようなシステムは、スレッドは安価でプロセスは高価であると言われています。他のオペレーティングシステムでは、アドレス空間の切り替えのコストを除けば、それほど大きな違いはありません。一部のアーキテクチャ(特にx86)では、アドレス空間の切り替えによってTLB(変換ルックアサイドバッファ)のフラッシュが発生します。
スレッドとプロセスの長所と短所は以下のとおりです。
オペレーティングシステムは、スレッドをプリエンプティブまたは協調的にスケジュールします。マルチユーザーオペレーティングシステムは、コンテキストスイッチによる実行時間のよりきめ細かな制御のために、一般的にプリエンプティブマルチスレッドを好みます。しかし、プリエンプティブスケジューリングでは、プログラマが予期しないタイミングでスレッドのコンテキストスイッチが発生する可能性があり、ロックコンボイ、優先度反転、その他の副作用を引き起こすことがあります。一方、協調マルチスレッドでは、スレッドが実行制御を放棄することで、スレッドが完了するまで実行されることが保証されます。協調マルチタスクのスレッドがリソース待ちでブロックしたり、集中的な計算中に実行制御を譲渡せずに他のスレッドを飢餓状態にしたりすると、問題が発生する可能性があります。
2000年代初頭まで、ほとんどのデスクトップコンピュータはシングルコアCPUを1つしか搭載しておらず、ハードウェアスレッドはサポートされていませんでしたが、スレッド間の切り替えは一般的にプロセス全体のコンテキストスイッチよりも高速だったため、そのようなコンピュータでもスレッドは使用されていました。2002年、IntelはPentium 4プロセッサにハイパースレッディングという名称で同時マルチスレッドのサポートを追加しました。2005年には、デュアルコアのPentium Dプロセッサを、AMDはデュアルコアのAthlon 64 X2プロセッサを発表しました。
シングルプロセッサシステムでは、一般的にタイムスライシングによってマルチスレッドが実装されます。つまり、中央処理装置(CPU)が異なるソフトウェアスレッドを切り替えます。このコンテキスト切り替えは通常、ユーザーがスレッドやタスクが並列に実行されていると認識するほど頻繁に行われます(一般的なサーバー/デスクトップオペレーティングシステムでは、他のスレッドが待機している間、スレッドの最大タイムスライスは100~200ミリ秒に制限されることがよくあります)。マルチプロセッサまたはマルチコアシステムでは、複数のスレッドを並列に実行でき、各プロセッサまたはコアが同時に別のスレッドを実行します。ハードウェアスレッドを備えたプロセッサまたはコアでは、個別のソフトウェアスレッドを個別のハードウェアスレッドで同時に実行することもできます。
カーネル内のスケジューラ可能なエンティティとユーザーが 1:1 で対応するスレッド[ 13 ]は、最も単純なスレッド実装です。OS /2とWin32 は最初からこのアプローチを使用しており、LinuxではGNU C ライブラリがこのアプローチを実装しています ( NPTLまたは古いLinuxThreadsを介して)。このアプローチは、 Solaris、NetBSD、FreeBSD、macOS、およびiOSでも使用されています。
M :1 モデルでは、すべてのアプリケーション レベルのスレッドが 1 つのカーネル レベルのスケジュールされたエンティティにマッピングされます。[ 13 ]カーネルはアプリケーション スレッドについて何も知りません。このアプローチでは、コンテキスト スイッチを非常に高速に実行でき、さらに、スレッドをサポートしていない単純なカーネルでも実装できます。ただし、大きな欠点の 1 つは、マルチ スレッドプロセッサまたはマルチ プロセッサコンピュータのハードウェア アクセラレーションの恩恵を受けられないことです。同時にスケジュールされるスレッドは 1 つ以下です。[ 13 ]例えば、スレッドの 1 つが I/O 要求を実行する必要がある場合、プロセス全体がブロックされ、スレッドの利点を使用できません。GNU Portable Threads は、 State Threadsと同様に、ユーザー レベルのスレッドを使用します。
M : N は、 M個のアプリケーション スレッドをN個のカーネル エンティティ[ 13 ] 、つまり「仮想プロセッサ」にマッピングします。これは、カーネル レベル (「1:1」) とユーザー レベル (「 N :1」) のスレッド処理の中間的なものです。一般に、「M : N」スレッド システムは、カーネル スレッドまたはユーザー スレッドのどちらよりも実装が複雑です。これは、カーネル スペース コードとユーザー スペース コードの両方に変更を加える必要があるためです。M:N 実装では、スレッド ライブラリが利用可能なスケジューラ可能なエンティティにユーザー スレッドをスケジューリングする責任を負います。これにより、システム コールを回避するため、スレッドのコンテキスト スイッチが非常に高速になります。ただし、これにより複雑さが増し、優先順位の逆転が発生する可能性が高くなり、ユーザー ランド スケジューラとカーネル スケジューラ間の広範な (そして高価な) 調整がないと、スケジューリングが最適ではなくなります。
SunOS 4.x では、軽量プロセス(LWP) が実装されました。NetBSD 2.x + およびDragonFly BSDでは、LWP がカーネル スレッド (1:1 モデル) として実装されました。SunOS 5.2 から SunOS 5.8 および NetBSD 2 から NetBSD 4 では、2 レベル モデルが実装され、各カーネル スレッドで 1 つ以上のユーザー レベル スレッドが多重化されました (M:N モデル)。SunOS 5.9 以降および NetBSD 5 では、ユーザー スレッドのサポートが削除され、1:1 モデルに戻りました。[ 14 ] FreeBSD 5 では、M:N モデルが実装されました。FreeBSD 6 では、1:1 と M:N の両方がサポートされ、ユーザーは /etc/libmap.conf を使用して特定のプログラムで使用する方を選択できました。FreeBSD 7 以降では、1:1 がデフォルトになりました。FreeBSD 8 では、M:N モデルはサポートされなくなりました。
コンピュータプログラミングにおいて、シングルスレッドとは一度に1つの命令を処理することである。 [ 15 ]変数の意味論とプロセス状態の形式分析では、シングルスレッドという用語は、「単一のスレッド内でのバックトラッキング」を意味するように異なる意味で使用されることがあり、これは関数型プログラミングのコミュニティでよく見られる。[ 16 ]
マルチスレッドは主にマルチタスクオペレーティングシステムで採用されています。マルチスレッドは、1つのプロセス内で複数のスレッドが共存することを可能にする、広く普及しているプログラミングおよび実行モデルです。これらのスレッドはプロセスのリソースを共有しますが、それぞれ独立して実行できます。スレッドプログラミングモデルは、開発者に並行実行の有用な抽象化を提供します。マルチスレッドは、マルチプロセッシングシステム上で並列実行を可能にするために、1つのプロセスにも適用できます。
マルチスレッドライブラリは、関数を引数として受け取る新しいスレッドを作成する関数呼び出しを提供する傾向があります。すると、渡された関数の実行を開始し、関数が戻り値を返すと終了する並行スレッドが作成されます。スレッドライブラリは、データ同期機能も提供します。
同一プロセス内のスレッドは、同じアドレス空間を共有します。これにより、並行して実行されるコードは密接に結合し、 IPCのオーバーヘッドや複雑さなしにデータを便利に交換できます。しかし、スレッド間で共有される場合、更新に複数のCPU命令を必要とする単純なデータ構造であっても、競合状態が発生しやすくなります。2つのスレッドが同時にデータ構造を更新しようとして、予期せずデータ構造が変更されてしまう可能性があるのです。競合状態によって引き起こされるバグは、再現や特定が非常に困難な場合があります。
これを防ぐために、スレッドアプリケーションプログラミングインターフェイス(API)は、同時アクセスからデータ構造をロックするためのミューテックスなどの同期プリミティブを提供します。単一プロセッサシステムでは、ロックされたミューテックスに侵入したスレッドはスリープする必要があり、コンテキストスイッチが発生します。マルチプロセッサシステムでは、スレッドは代わりにスピンロックでミューテックスをポーリングすることができます。これらの方法はいずれもパフォーマンスを低下させ、特にロックの粒度が細かすぎる場合、対称型マルチプロセッシング(SMP)システムのプロセッサがメモリバスを巡って競合する原因となる可能性があります。
その他の同期APIには、条件変数、クリティカルセクション、セマフォ、モニターなどがあります。
スレッドを扱う一般的なプログラミングパターンとして、スレッドプールがあります。これは、起動時に一定数のスレッドを作成し、タスクが割り当てられるまで待機させるものです。新しいタスクが到着すると、スレッドは起動し、タスクを完了すると再び待機状態に戻ります。これにより、タスクごとに比較的コストのかかるスレッドの作成と破棄を行う必要がなくなり、アプリケーション開発者はスレッド管理をライブラリやオペレーティングシステムに任せることができるため、スレッド管理の最適化が容易になります。
すべてのスレッドがビジー状態の場合、追加の作業項目は、実行可能なスレッドが見つかるまでキューに入れられます。[ 17 ]
マルチスレッドアプリケーションは、シングルスレッドアプリケーションと比較して、以下の利点があります。
マルチスレッドアプリケーションには、次のような欠点があります。
多くのプログラミング言語は、何らかの形でスレッド処理をサポートしています。
{{cite AV media}}: CS1 maint: bot: 元の URL の状態が不明です (リンク)