最新のオペレーティングシステムとマイクロプロセッサは、マルチタスクを長い間サポートしており、同時に、これらの各タスクは複数のスレッドで実行できます。 これにより、コンピューティングパフォーマンスが大幅に向上し、ユーザーアプリケーションとサーバーをより適切にスケーリングできますが、これに代価を支払う必要があります。プログラムの開発とデバッグは複雑です。
この記事では、POSIXスレッドを知ってから、LinuxでPOSIXスレッドがどのように機能するかを学習します。 同期と信号のジャングルに入ることなく、Pthreadの主要な要素を検討してください。 だから、ボンネットの下に流れます。
一般的な情報
1つのプロセスで実行される複数のスレッドはスレッドと呼ばれ、これはスレッド識別子、カウンター、レジスター、およびスタックで構成されるCPUロードの基本単位です。 1つのプロセス内のスレッドは、コード、データ、およびさまざまなリソースのセクションを共有します:オープンファイル記述子、プロセス資格情報、シグナル、 umask
、 nice
値、タイマーなど。
すべての実行可能プロセスには、少なくとも1つの実行スレッドがあります。 パフォーマンスのスレッドを追加してもパフォーマンスが向上せず、プログラムが複雑になるだけの場合、一部のプロセスはこれに限定されます。 ただし、このようなプログラムは毎日比較的小さくなっています。
複数の実行スレッドの利点は何ですか? 読み込まれたWebサーバー(habrahabr.ruなど)を使用します。 サーバーが各http
リクエストを処理するための個別のプロセスを作成した場合、ページがロードされるまで永遠に待機します。 新しいプロセスを作成することは、OSにとって高価な喜びです。 copy-on-writeの最適化を考慮しても、 fork
およびexec
システムコールはメモリページの新しいコピーとファイル記述子のリストを作成します。 一般に、OSカーネルは、新しいプロセスよりもはるかに高速に新しいスレッドを作成できます。
カーネルは、データページ、スタックおよびヒープを含む親プロセスのメモリセグメントに書き込むときにコピーする必要があります。 プロセスは多くの場合fork
呼び出しを行い、そのexec
直後に、 fork
呼び出し中にページをコピーすることは不必要な無駄になりますexec
実行後にそれらを破棄する必要があります。 まず、ページテーブルエントリは、親プロセスの物理メモリの同じページを指し、ページ自体は読み取り専用としてマークされます。 ページのコピーは、 変更したいまさにその瞬間に行われます 。
記録中のコピー中に共有メモリページを変更する前後のページテーブル。
プロセス実行の並列スレッド数、プログラムアルゴリズム、生産性の向上の間にはパターンがあります。 この依存関係はアムダール法と呼ばれます。
プロセスの並列化に関するアムダールの法則。
図に示す式を使用して、N個のプロセッサとシステムのどの部分を並列化できないかを示す係数Fを使用して、システムパフォーマンスの最大改善を計算できます。 たとえば、コードの75%が並行して実行され、25%が順次実行されます。 この場合、プログラムの1.6倍の加速はデュアルコアプロセッサで達成され、2.28571倍は4コアプロセッサで達成され、Nが無限に向かう場合の加速制限は4です。
スレッドをカーネルモードにマッピングする
Windows、Linux、Mac OS X、およびSolarisを含むほとんどすべての最新のオペレーティングシステムは、カーネルモードのスレッド制御をサポートしています。 ただし、スレッドはカーネルモードだけでなく、ユーザーモードでも作成できます。 このレベルを使用する場合、カーネルはスレッドの存在を認識しません。すべてのスレッド制御は、特別なライブラリを使用するアプリケーションによって実装されます。 ユーザースレッドは、カーネルモードのスレッドとは異なる方法でマッピングされます。 合計で3つのモデルがありますが、そのうち1が最も一般的に使用されています。
マッピングN:1
このモデルでは、複数のユーザースレッドが1つのOSカーネルスレッドにマップされます。 すべてのフロー制御は特別なユーザーライブラリによって実行され、これがこのアプローチの利点です。 欠点は、1つのスレッドがブロッキング呼び出しを行うと、プロセス全体が禁止されることです。 以前のバージョンのSolaris OSはこのモデルを使用していましたが、その後、強制的に放棄しました。
1:1ディスプレイ
これは、プロセスで作成された各スレッドがOSカーネルスケジューラーによって直接制御され、カーネルモードで1つの単一のスレッドにマップされる最も単純なモデルです。 アプリケーションが制御されていないスレッドを生成してOSをオーバーロードしないように、OSでサポートされるスレッドの最大数に制限を課します。 このストリーム表示方法は、LinuxおよびWindowsでサポートされています。
Mマッピング:N
このアプローチでは、M個のユーザースレッドが、同じかまたは少ないN個のカーネルスレッドに多重化されます。 他の2つのモデルの悪影響は克服されます。スレッドは本当に並列に実行され、OSがそれらの総数に制限を導入する必要はありません。 ただし、このモデルをプログラミングの観点から実装することは非常に困難です。
POSIXストリーム
1980年代後半から1990年代初頭にいくつかの異なるAPIがありましたが、1995年にPOSIX.1cは POSIXストリームを標準化しました。これは後にSUSv3仕様の一部になりました 。 現在、マルチコアプロセッサはデスクトップPCやスマートフォンにも浸透しているため、ほとんどのマシンは低レベルのハードウェアサポートを備えており、複数のスレッドを同時に実行できます。 昔は、シングルコアCPUでのスレッドの同時実行は非常に独創的でしたが、非常に効果的な錯覚でした。
Pthreadは、Cで一連のタイプと関数を定義します。
-
pthread_t
スレッド識別子。 -
pthread_mutex_t
ミューテックス; -
pthread_mutexattr_t
ミューテックス属性オブジェクト -
pthread_cond_t
条件変数 -
pthread_condattr_t
条件変数の属性オブジェクト。 -
pthread_key_t
スレッド固有のデータ。 -
pthread_once_t
動的初期化制御のコンテキスト。 -
pthread_attr_t
スレッド属性のリスト。
従来のUnix APIでは、 errno
最後のエラーコードはグローバルなint
変数です。 ただし、これは、複数の実行スレッドを持つプログラムには適していません。 実行可能スレッドの1つでの関数呼び出しがグローバル変数errno
エラーで終了した状況では、他のスレッドが現在エラーコードをチェックして困惑しているという事実により、競合状態が発生する可能性があります。 UnixおよびLinuxでは、この問題は、 errno
が各スレッドに独自の可変lvalue
を設定するマクロとして定義されているという事実によって回避されました。
男からerrno
errno変数は、ISO C標準で可変lvalue intとして定義されており、明示的に宣言されていません。 errnoはマクロの場合があります。 errno変数は、スレッドのローカル値です。 あるスレッドでの変更は、別のスレッドでの値に影響しません。
ストリーム作成
ストリーム関数は最初に作成されます。 次に、 pthread.hヘッダーファイルで宣言されたpthread_create()
関数によって新しいスレッドが作成されます。 さらに、呼び出し元は、ストリーム関数と並行していくつかのアクションを実行し続けます。
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start)(void *), void *arg);
成功した場合、 pthread_create()
はコード0を返し、ゼロ以外の値はエラーを示します。
-
pthread_create()
呼び出しの最初のパラメーターは、pthread_t
型の作成されたスレッドの識別子を格納するためのアドレスです。 -
start
引数は、唯一の変数として型のないポインターをとるストリーミングvoid *
関数へのポインターです。 -
arg
引数は、ストリーム引数を含む型のないポインターです。 ほとんどの場合、arg
はグローバル変数またはダイナミック変数を指しますが、呼び出された関数が引数を必要としない場合、arg
をNULL
として指定できNULL
。 -
attr
引数は、pthread_attr_t
スレッド属性への型なしポインターでもあります。 この引数がNULL
場合、ストリームはデフォルトの属性で作成されます。
次に、マルチスレッドプログラムの例を考えてみましょう。
#include <pthread.h> #include <stdio.h> int count; /* */ int atoi(const char *nptr); void *potok(void *param); /* */ int main(int argc, char *argv[]) { pthread_t tid; /* */ pthread_attr_t attr; /* */ if (argc != 2) { fprintf(stderr,"usage: progtest <integer value>\n"); return -1; } if (atoi(argv[1]) < 0) { fprintf(stderr," %d \n",atoi(argv[1])); return -1; } /* */ pthread_attr_init(&attr); /* */ pthread_create(&tid,&attr,potok,argv[1]); /* */ pthread_join(tid,NULL); printf("count = %d\n",count); } /* */ void *potok(void *param) { int i, upper = atoi(param); count = 0; if (upper > 0) { for (i = 1; i <= upper; i++) count += i; } pthread_exit(0); }
Pthreadライブラリをプログラムに接続するには、 -lpthread
オプションを渡す必要があり-lpthread
。
gcc -o progtest -std=c99 -lpthread progtest.c
pthread_join
スレッドへの参加については、後ほど説明します。 pthread_t tid
文字列は、スレッド識別子を指定します。 関数の属性はpthread_attr_init(&attr)
によって設定されます。 それらを明示的に指定しなかったため、デフォルト値が使用されます。
フロー完了
スレッドは次の場合にタスクを完了します。
- ストリーム関数は、計算の結果を返します。
- スレッド
pthread_exit()
実行を終了する呼び出しの結果として; - スレッドキャンセルコールの結果として
pthread_cancel()
; - スレッドの1つが
exit()
呼び出しを行います -
main()
関数のメインスレッドがreturn
実行します。この場合、プロセスのすべてのスレッドが突然フォールドされます。
構文は、ストリームを作成するよりも簡単です。
#include <pthread.h> void pthread_exit(void *retval);
後者の場合、 main()
関数の最上位のスレッドが単にexit()
またはreturn
代わりにpthread_exit()
を実行すると、残りのスレッドは何も起こらなかったように実行され続けます。
ストリームを待っています
pthread_join()
関数は、 THREAD_ID
示されるスレッドTHREAD_ID
待機THREAD_ID
ます。 このスレッドがそれまでにすでに完了している場合、関数はすぐに値を返します。 この関数の意味は、スレッドを同期することです。 次のようにpthread.h
で宣言されています。
#include <pthread.h> int pthread_join (pthread_t THREAD_ID, void ** DATA);
成功した場合、 pthread_join()
はコード0を返し、ゼロ以外の値はエラーを示します。
DATA
ポインターがNULL
と異なる場合、ストリームからpthread_exit()
関数またはストリーム関数のreturn
命令を介してreturn
れるデータがそこに配置されます。 複数のスレッドは、1つのスレッドが完了するのを待つことはできません。 これを行おうとすると、1つのスレッドが成功し、他のすべてのスレッドはESRCHエラーで失敗します。 pthread_join()
すると、アプリケーションはスレッドに関連付けられたスタックスペースを使用できるようになります。
ある意味では、 pthread_joini()
はwaitpid()
呼び出すようなもので、プロセスの完了を待機しますが、いくつかの違いがあります。 最初に 、すべてのスレッドはピアツーピアであり、それらの間に階層順序はありませんが、プロセスはツリーを形成し、親子階層に従属します。 そのため、スレッドAがスレッドBを生成し、Bにパッチを適用し、 pthread_join()
関数を呼び出した後、関数AがBの終了を待機する、またはその逆の状況が発生する可能性があります。 次に 、 waitpid(-1, &status, options)
呼び出しwaitpid(-1, &status, options)
可能なように、 スレッドの完了を期待することはできません。 また、 pthread_join()
非ブロッキング呼び出しを行うこともできません。
ストリームの早期終了
プロセスを管理するときと同じように、プロセスを時期尚早に終了することが必要な場合があり、マルチスレッドプログラムはスレッドの1つを時期尚早に終了する必要がある場合があります。 pthread_cancel
関数を使用して、スレッドを早期に終了できます。
int pthread_cancel (pthread_t THREAD_ID);
成功した場合、 pthread_cancel()
は0を返し、ゼロ以外の値はエラーを示します。
pthread_cancel()
はすぐに戻り、スケジュールより早くスレッドを終了できますが、スレッドを強制終了する手段とは呼ばれないことを理解することが重要です。 実際、スレッドはpthread_cancel()
呼び出しに応答して完了の瞬間を独立して選択できるだけでなく、それを完全に無視することもできます。 pthread_cancel()
関数の呼び出しは、スレッドの早期終了を実行する要求と見なす必要があります。 したがって、スレッドを削除することが重要な場合は、 pthread_join()
関数でスレッドが完了するまで待つ必要があります。
ストリームの作成と取り消しの小さな図。
pthread_t tid; /* */ pthread_create(&tid, 0, worker, NULL); … /* */ pthread_cancel(tid);
ここで、この呼び出しの結果の意性と予測不能性が支配するという印象を与えないために、早期終了の呼び出しを受信した後のストリームの動作を決定するパラメーターのテーブルを検討します。
ご覧のとおり、完全にかけがえのないスレッドがあり、デフォルトの動作は遅延完了です 。これは、完了時に発生します。 そして、まさにこの瞬間が来たことをどうやって知るのでしょうか? pthread_testcancel
ヘルパー関数pthread_testcancel
。
while (1) { /* - */ /* --- */ /* - ? */ pthread_testcancel(); }
ストリームを切断する
デフォルトのスレッドは、 pthread_join()
を呼び出して、完了するのを待つことで参加できます。 ただし、場合によっては、ストリームの終了のステータスと戻り値は興味を引くものではありません。 必要なのは、フローを完了し、リソースをOSに自動的にアップロードすることだけです。 このような場合、 切断されたスレッドを指定し、 pthread_detach()
呼び出しを使用します。
#include <pthread.h> int pthread_detach(pthread_t thread);
正常に実行された場合、 pthread_detach()
は0を返し、ゼロ以外の値はエラーを示します。
切断されたストリームは文です。 pthread_join()
を呼び出して完了ステータスやその他の利点を取得することで、インターセプトできなくなりました。 また、切断状態を元に戻すことはできません。 充填のための質問。 スレッドの終了がpthread_join()
呼び出しによってインターセプトされない場合はどうなりますか?これは、切断されたスレッドが終了するシナリオとどう違いますか? 最初のケースではゾンビストリームを取得し、2番目のケースではすべてが正常になります。
スレッドとプロセス
最後に、マルチスレッドアプリケーションを設計するか、1つのスレッドで複数のプロセスで実行するかというトピックに関するいくつかの考慮事項を検討することを提案します。 まず、並列マルチスレッドの利点。
記事の最初の部分で、これらの利点を既に指摘しているので、簡単に簡単にリストします。
- ストリームは、プロセスと比較して非常に単純にデータを交換します。
- OS用のスレッドの作成は、プロセスの作成よりも簡単で高速です。
次に、欠陥について少し説明します。
- 複数のスレッドでアプリケーションをプログラミングする場合、関数のストリームの安全性を確保する必要があります-いわゆる スレッドセーフ 。 多くのプロセスで実行されるアプリケーションには、これらの要件はありません。
- スレッドは共通のアドレス空間を共有するため、1つの重要なスレッドが他のスレッドを損傷する可能性があります。 プロセスは互いに分離されています。
- ストリームはアドレス空間で互いに競合します。 スレッドのスタックとローカルストレージ。プロセスの仮想アドレス空間の一部をキャプチャし、他のスレッドからアクセスできないようにします。 組み込みデバイスの場合、この制限は重大になる可能性があります。
スレッドのテーマはほとんど底なしです。スレッドの操作の基礎でさえ、いくつかの講義を描くことができますが、Linuxのマルチスレッドアプリケーションの構造を研究するのに十分なことは既に知っています。
使用材料と追加情報
- Michael Kerrisk Linuxプログラミングインターフェイス。
- Abraham Silberschatz、Peter B. Galvin Greg Gagne、 オペレーティングシステムコンセプト9th ed。
- Nikolay Ivanov Linux 2-tエディションの独学プログラミング。
- アンドリュータネンバウムコンピュータアーキテクチャ 。