スレッド、clone、futexとCMPXCHG
これは、スレッド(もしくはpthread)に関連するトピックについて、自分が過去に断続的に取ったメモのページです。理解を整理するためにまとめました。
pthreadの内部
pthreadsはcloneを呼び出す。これによりtask_structが作成される。
メモ:
-
clone()には次のような異なるフラグが存在する:-
CLONE_VM: メモリ空間(fork()はメモリ空間を共有しない) -
CLONE_FS: ファイルシステム -
CLONE_FILES: ファイルディスクリプタ -
CLONE_SIGHAND: シグナルハンドラ -
CLONE_THREAD: スレッド -
CLONE_PARENT_SETTID: -
CLONE_CHILD_CLEARTID:
-
メモ: libc vs カーネルのシステムコール
カーネルのシステムコールは移植性と安全性のためにlibcが標準インターフェイスとなる。
man 2 [名前]はカーネルのシステムコールを説明し、man 3 [名前]はlibcの関数を説明するが、man 2でもライブラリセクションにlibcが表示されることが多い。libc関数が単なるラッパーか、システムコールの上にさらにロジックを持っているかを調べるためには、その関数の内部や知識が必要。しかし、関数が
man 3にしか存在しない場合、それはlibcの中でシステムコールをラップしているカスタム関数であることが分かる。
カーネルの観点
プロセスとスレッドの両方は、カーネル内でtask_structとして扱われる。
-
スケジューリング: 統一タスクスケジューリング
カーネルはプロセスとスレッドを異なる方法でスケジュールしない。
-
プリエンプティブマルチタスキング: OSはスレッド間で中断し、切り替えを行うことができる。(OSはハードウェアのタイマー割り込みを使用。)
-
マルチタスキングの基準:
-
タイムスライス(クォンタム)
-
I/O操作
-
優先順位
-
-
完全公平スケジューラ(CFS)がデフォルト。リアルタイムのための他のポリシーとして、
SCHED_FIFOとSCHED_RRなども存在。
-
-
スレッドの状態(レジスタ、プログラムカウンタ、スタックポインタなど)は
スレッド制御ブロック(TCB)として保存され、プロセスの状態はプロセス制御ブロック(PCB)として保存される。 -
スレッドとプロセスの違い:
-
スレッドの
task_structはプロセスのスレッドグループの一部となる。 -
スレッドの構造体は共有リソースを指す。
-
カーネルスケジュールの制御
-
CPUアフィニティ: スレッド/プロセスが実行できるCPUコアを決定します。
スレッドを特定のコアにバインドし、コンテキストスイッチを減少させ、キャッシュ利用効率を向上させる為に使用。
-
ポリシー: 異なるポリシーを設定できる。
sched_setscheduler(pid, policy, param)とsched_getscheduler(pid)-
SCHED_OTHER: デフォルトのLinuxタイムシェアリングスケジューラ -
SCHED_FIFO: 先入れ先出しのリアルタイムスケジューリング -
SCHED_RR: ラウンドロビンのリアルタイムスケジューリング -
SCHED_IDLE: バックグラウンドタスク用の非常に低い優先度 -
SCHED_BATCH: バッチ処理ジョブに適した設定
-
-
優先順位: 優先順位も設定できる。
setpriority(which, who, priority)とgetpriority(which, who)-
type:PRIO_PROCESSなど -
who: 識別子 -
priority: 新しい優先度の値
-
-
コマンド:
niceとreniceは優先度を変更する。- 値は
-20(最も高い優先度)から19(最も低い優先度)の範囲。
- 値は
-
pthread:
-
pthread_setschedparamとpthread_getschedparam: スケジューリングポリシーとパラメタ -
pthread_attr_setschedpolicyとpthread_attr_setschedparam: スレッドの属性
-
ユーザーレベルスレッド
-
コルーチン: 実行を一時停止し、後で再開できる関数。Python(async/await)、Kotlin、Luaで一般的。
-
ファイバー: コルーチンに似ているが、一般的により汎用的。RubyやC++にライブラリあり。
-
m:nスレッド:m個のユーザーレベルスレッドがそれより少数のn個のカーネルスレッドにマッピングされます。-
利点: 柔軟性 /
nを調整可能 / コンテキストスイッチなし / ブロッキング操作を避ける処理可能 -
欠点: 複雑 / デバッグ / 限定的なOSサポート(LinuxとWindowsは
1:1スレッドモデルを使用) -
OSレベル: 単純化そして効率化のためほとんど
1:1(古いSolarisとGNU Portable Threadsはm:nマッピングを提供) -
例:
-
Go:
goroutinesは少数のOSスレッドにスケジュールされる。 -
Erlang: BEAM仮想マシンを通じて、軽量プロセスは少数のOSスレッドにスケジュールされる。
-
NodeJS: モデルは違うが、イベントループ(イベント駆動アーキテクチャ)を通じて少数のスレッドで複数のタスクが並行して処理される。
-
-
1:1モデルのみの言語JavaやRustは
m:nやグリーンスレッドのモデルを言語として提供せず、1:1のOSネイティブなスレッドの呼び出しのみを言語としてサポートする。それに伴いこれらの言語では
m:n、グリーンスレッドやファイバーの機能は言語サポート外のライブラリとして実装、提供される。
同期機構
複数のスレッドがある場合、競合を処理する必要あり。pthreadはミューテックス、条件変数、セマフォなどの同期プリミティブを提供。
-
Mutex (
pthread_mutex_t): 一度に1つのスレッドのみが特定のコード(メモリ)にアクセスできるようにする。-
futexを使用。 -
所有権がある: ロックの所有者スレッドだけが更新できる。
-
実装: 内部ステート(カーネルまたはOSがスレッドのblockとwakeupのメカニズムを提供)
-
-
Condition variable (
pthread_cond_t): 特定の条件が満たされるまでスレッドをブロックする。通常はミューテックスと組み合わせて使用される。- 関連するミューテックスを解放し、
futexを使用して他のスレッドからのシグナルやブロードキャストを待つ。
- 関連するミューテックスを解放し、
-
Semaphore (
sem_t): 共有リソースへのアクセスを制御。-
ミューテックスに似ていて、セマフォカウントがゼロであり、スレッドがそれを減少させようとすると、スレッドは
futexを使用して待機する。カウントが増加すると、futexを使用して待機しているスレッドの1つを起こす。 -
所有権なし:どのスレッドからでも変更可能。
-
使用例: リソースプール(例: DB接続プール)/ producer/consumerの可用性管理
-
実装: カウンタ + キュー
-
Futex
“Fast userspace mutex”の略。競合が発生した場合のみ、futexはカーネルとやり取りする。
メモ:
futexはカーネルの関与を最小限に抑えるように設計されているため、一見カーネルの一部ではないように見えるが、実際にはカーネルの構成要素。
-
高速パス: ミューテックスが解放されている→ロックが取得される(ユーザー空間のみ)
-
遅延パス: ミューテックスが解放されていない→スレッドがシステムコールを行い、カーネルはミューテックスが解除されるまでスレッドをスリープさせる。
-
futexは、スレッドを効率的にスリープさせ、ウェイクアップするメカニズムを提供し、カーネルが待機キューを管理する。-
futex_wait: スレッドがfutex変数(通常は特定のメモリ位置)を待っていることを示すために使用される。条件が満たされていない場合(例: ロックが既に保持されている場合)、カーネルはスレッドをスリープさせる。 -
futex_wake: futex変数を待機している1つまたは複数のスレッドを起こすために使用される。ロックが解除されたり条件が変わったりしたとき、futex_wakeを使用して待機中のスレッドに通知し、それによってスレッドが実行を続ける。
-
-
コンペア・アンド・スワップ(CAS): ロック取得に使用されるアトミック操作。
-
X86/64:
EAX,EBX,ECX(汎用レジスタ)またはRAX,RBX,RCX(64ビット汎用レジスタ)などを使い、CMPXCHG destination, sourceの命令。 -
Arm: 汎用レジスタ(例:
r0,r1, etc.)上でLDREX(ロード専用)およびSTREX(ストア専用)の命令。LDREX r0, [address] ; 'address'の値を'r0'にロード CMP r0, old_value ; ロードした値を'old_value'と比較 STREXEQ r1, new_value, [address] ; 等しい場合、'new_value'を'address'にアトミックに格納
-