コース内容#
プロセス間通信は複雑 [メモリ空間は独立]、切り替えコストが高い [時間局所性] ため、スレッドが発明されました。
スレッド#
プロセスの分岐 [pthread]、本質的には軽量なプロセスです。
- 通信が便利で、同一プロセス内の複数のスレッドがメモリを共有します。
- 切り替えコストが低く、メモリが共有されているため、スレッドを切り替える際にキャッシュを置換する必要がありません。
pthread_create#
新しいスレッドを作成します。
- man pthread_create
- プロトタイプ
- thread:スレッド ID [注意:数値型ではなく、直接 == で比較できない、pthread_equal を参照]
- attr:属性
- arg は start_routine の引数です。
- 説明
- 新しいスレッドを開始し、start_route 関数を実行します。
- start_route 関数は 1 つの arg パラメータのみを受け取ります [複数のパラメータは構造体でラップできます]。
- スレッドには4 つの終了方法があります [ツールとして、死亡方法は重要です]。
- ① 自殺:自分で pthread_exit を呼び出す。
- 同一プロセス内のスレッドは pthread_join を使用してその死亡状態を受け取ることができます [wait に似ています]。
- ② 正常死亡:start_routine 関数から戻る。
- pthread_exit 方式と同等です。
- ③ 他殺:pthread_cancel。
- ④ 同帰于尽:プロセス内のスレッドが exit を呼び出すか、メインスレッドが main 関数から戻る。
- [PS] もしスレッドがメモリ崩壊を引き起こすと、同帰于尽の効果が生じる可能性が高く、すべてのスレッドが死亡します。
- ① 自殺:自分で pthread_exit を呼び出す。
- attr は NULL にすることができ、デフォルト属性に対応します。
- 成功すると、スレッド ID が thread 変数に保存され、以降はこの ID を使用してアクセスできます [ファイルディスクリプタに似ています]。
- 戻り値
- 0、成功;それ以外は失敗です。
——3 つのスレッド終了方法 ——#
pthread_exit#
スレッド自殺
- man pthread_exit
- ❗ スレッド自殺時、retval を join するスレッドに渡します [スレッドはデフォルトで参加可能です]。
- pthread_cleanup_push で登録された関数を実行した後、スレッド特有のデータを解放します。
- プロセス内で共有されるリソースは解放されません [兄弟スレッドが存在するため]。
- atexit で登録された関数は呼び出されません [これはプロセスに属します]。
- 最後のスレッドが終了した後、プロセスは exit (0) で終了し、プロセス共有リソースを解放し、atexit で登録された関数を実行します。
- 【注意】スレッドとプロセスの関係
pthread_cancel#
スレッドにキャンセルリクエストを送信します [他殺]。
- man pthread_cancel
- スレッドキャンセルの可能性と時間は、2 つの属性:state、type に依存します。
- state
- キル可能 [デフォルト]
- キル不可能:この場合、受け取ったキャンセル命令はキューに並びます。
- type
- 遅延 [デフォルト]:スレッドの次の呼び出しまで待機します。
- 非同期:即時ですが、システムは保証できません。
exit [プロセス関連]#
通常のプロセスを終了します。
- man exit
- 親プロセスに渡される値は:status & 0377
- 注意:0377 は 8 進数で、2 進数の 8 つの 1 に対応し、status の下位 8 ビットのみを保持します。
- atexit および on_exit で登録された関数は、登録された逆の順序で呼び出されます。
- ネスト可能:登録された関数の中にさらに登録があり、呼び出しリストの最前面に配置されます。
- 登録された関数が戻らない場合、たとえば_exit を呼び出すか、シグナルで自殺した場合、残りの関数は呼び出されず、exit 関連の処理も禁止されます。
- 複数回登録された関数は、複数回呼び出されます。
- ⭐ exit 後はすべての標準 IO ストリームをフラッシュして閉じます。
—— スレッド状態の監視関連 ——#
pthread_join#
スレッドの終了を待機します。
- man pthread_join
- プロセス内の wait 関数に類似しています。
- retval はスレッドの終了状態を受け取ります。
- スレッドが自殺した場合、pthread_exit 内の retval 値をコピーします。
- スレッドが他殺された場合、PTHREAD_CANCELED に設定されます。
- [考察] ここでの retval は二次ポインタ、つまりポインタのポインタです。なぜ?
- 表面的な理由:pthread_exit 内の retval はポインタであり、慣例に従ってここでは二次ポインタを使用する必要があります [同様に、int データを受け取る場合、ここでは int * を使用します]。
- さらに深い理由:渡されたポインタを変更できるようにするためです。
- ここにブログも言及しています:pthread_join () 関数の第二引数がなぜ二次ポインタであるかの考察——CSDN
pthread_detach#
スレッドを分離します。
- man pthread_detach
- スレッドが分離されると、その終了時にシステムが自動的にリソースを回収し、他のスレッドが終了を待つ必要がなくなります。
- 一般的にpthread_selfと組み合わせて自分を分離します。
- pthread_self:呼び出しスレッド [自分] の ID を取得します。
- 参考:pthread_join () と pthread_detach () の詳細——CSDN
—— 追加 ——#
pthread_yield#
プロセッサを譲ります [processor]。
- man pthread_yield
- [sleep に似た効果]
- このメソッドは特定のシステムでのみ使用され、より標準的な使用法は:sched_yield です。
- 協調型システムでは、この関数を呼び出して CPU を積極的に譲ります。
- プリエンプティブシステムでは、カーネルがスケジューリングを行い、この関数はあまり意味がなく、sleep を直接使用できます。
- 協調型とプリエンプティブについては、4 高度なプロセス管理 ——スケジューラの分類を参照してください。
pthread_equal#
2 つのスレッドの ID を比較します [直接 == で比較できません]。
- man pthread_equal
- 等しい場合、非ゼロ値を返します。
スレッドプール#
一群のスレッドがプールで待機し、いつでも作業を行います。
基本構成要素👇
①タスクキュー:処理が必要なタスクを格納します。
- [循環キューがより良い]
- 基本操作:init、push、pop
② 複数のスレッド:常に準備が整い、応答と破棄の時間を短縮します。
③ スレッド機能:do_work ()
- while (1):タスクの追加を待機します。
-
- タスクキュー pop:タスクをポップしてスレッドが実行します。
-
- do-work():CPU内でタスクを実行します。
-
❗ 注意:push と pop の際は必ずロックをかけ、データ競合を防ぎます [飢えたスレッド]。
- コードデモを参照してください。
カーネルスレッド、ユーザースレッド#
スレッドは誰が生成したのか?スレッドモデル
- 両者の違いは主にスケジューリングにあります:カーネルスレッドはカーネルによってスケジュールされ、ユーザースレッドはユーザープロセスによってスケジュールされます。
- カーネルスレッドの利点
- ① 各カーネルスレッドには独自のタイムスライスがあります。したがって、複数のスレッドを持つプロセスは、より多くのプロセッサ時間を持ちますが、ユーザープロセスはユーザースレッドをいくつか分けても、より多くのプロセッサ時間を得ることはありません。
- ② もしカーネルスレッドがブロックされると、プロセス内の残りのスレッドは引き続き実行できます。もしユーザースレッドがブロックされると、プロセス全体がブロックされます。
- PS:もしカーネルスレッドが自分のプロセスに sleep 信号を送ると、このスレッドは引き続き実行できます。
- ユーザースレッドの利点
- ① 切り替えコストが低い。ユーザーモードからカーネルモードへの変換は関与しません。
- ② スケジューリングアルゴリズムは完全にプロセスによって制御されます。ユーザープロセスは独自のスケジューリングアルゴリズムを使用できるため、自主性が高いです。一方、カーネルスレッドのスケジューリングはユーザーにとってはブラックボックスです。
- したがって、両者の利点を組み合わせて、カーネルスレッドとユーザースレッドのハイブリッドスレッドを設計できます。
コードデモ#
簡単なマルチスレッドの使用#
- 注目:pthread_create 関数の使用。
- スレッドを作成した後、usleep がないか、usleep の時間が短すぎると、次のことが発生する可能性があります:
- メインスレッドが return し、すべての子スレッドが同帰于尽になります。
- この時、出力の一部が 2 回表示されることがあります、例えば①、② [③は正常な出力です]。
- 推測:出力バッファの問題で、スレッドが突然終了した際に、バッファの内容が再度出力された [バッファが更新される前に]。
- [PS] fflush を使用しても解決できない可能性があります、スレッドが突然終了したためです。
- 注意:すべてのスレッドが同じアドレスの値を操作できます。
スレッドプール#
thread_pool.h
- タスクキューの定義と基本操作。
thread_pool.c
- 注意:ロックの取得と解放、信号の送信;キューが満杯 / 空かの判断;ポインタがキューの末尾に達しているかの判断。
1.test.c
- データを格納するバッファは 2 次元配列で、スレッドがデータを読み取る際に、メインスレッドがアドレスを変更することを避けます。
- usleep の妙用:while (1) ループが CPU を過度に消費するのを避けます。
- pthread_detach は一般的に pthread_self と組み合わせて使用します。
- fgets:ファイルをバッファに読み込みます。
- man fgets
- 行単位で読み込みます。
- 出力効果
- 基本出力:push [タスクキュー出力];pop [タスクキュー出力] + do_work [スレッド出力]。
追加知識点#
- スレッド関連関数を含むファイルをコンパイルする際は、必ず - lpthread を使用してください。
ヒント#
- 作成されたプロセスは、主スレッドを表します。
- 単一 CPU でのマルチスレッドは順次実行されるため、単核 CPU ではマルチスレッドの使用は推奨されません。
- 参考:マルチスレッドプログラムにおける単核 CPU と多核 CPU の動作——cnblogs