本「Linux API。 包括的なガイド»

画像 こんにちは、habrozhiteli! 最近、Linuxオペレーティングシステムのソフトウェアインターフェイスに関するMichael Kerriskの基本的な作業をリリースしました。 この本は、Linuxを実行するシステムプログラミングAPIのほぼ完全な説明を提供しています。



「スレッド:はじめに」セクションを見てみましょう。 最初に、スレッドがどのように機能するかの簡単な概要を見てから、スレッドがどのように作成および完了されるかに焦点を当てます。 最後に、アプリケーション設計の2つの異なるアプローチ(マルチスレッドとマルチプロセス)を選択する際に考慮すべきいくつかの要因を検討します。





29.1。 短いレビュー



プロセスと同様に、スレッドは、単一のアプリケーション内で複数の並列タスクを同時に実行するためのメカニズムです。 図に示すように。 29.1では、1つのプロセスに複数のスレッドが含まれる場合があります。 それらはすべて、互いに独立して同じプログラム内で実行され、初期化/未初期化データとヒープセグメントを含むグローバルメモリを共有します(従来のUNIXプロセスはマルチスレッドプロセスの特殊なケースであり、1つのスレッドで構成されます)。



図 29.1いくつかの単純化が行われます。 特に、各スレッドのスタックの場所は、共有ライブラリおよび共有メモリ領域と交差する場合があります。 スレッドが作成され、ライブラリがロードされ、メモリの共通セクションがアタッチされた順序に依存します。 さらに、スレッドのスタックの場所は、Linuxディストリビューションによって異なる場合があります。



プロセス内のスレッドは同時に実行できます。 マルチプロセッサシステムでは、スレッドを並行して実行できます。 一方のスレッドがI / Oによりブロックされた場合、もう一方のスレッドが引き続き動作する場合があります(I / O専用の別のスレッドを作成する方が理にかなっていますが、代替のI / Oモデルの方が適していることがよくあります。詳細については、第59章を参照してください) 。



状況によっては、スレッドがプロセスより優先されます。 複数のプロセスを作成することにより、競争力のある実行に対する従来のUNIXアプローチを検討してください。 親プロセスが着信接続を受け入れ、各クライアントと通信する各fork()呼び出しで個別の子プロセスを作成するネットワークサーバーモデルの例を見てみましょう(セクション56.3を参照)。 これにより、複数の接続を同時に提供できます。 通常、このアプローチはうまく機能しますが、状況によっては次の制限につながります。



画像





グローバルメモリに加えて、スレッドは他の多くの属性も共有します(これは、個々のスレッドではなく、プロセス全体で属性がグローバルである場合です)。 その中には、以下にリストされている属性があります。



-プロセスとその親の識別子。

-プロセスグループとセッションの識別子。

-管理端末。

-プロセス資格情報(ユーザーおよびグループ識別子)。

-開いているファイルを処理します。

-fcntl()を呼び出して作成されたロックを記録します。

-信号のアクション。

-ファイルシステムに関連する情報:umask、現在およびルートディレクトリ。

-インターバルタイマー(setitimer())およびPOSIXタイマー(timer_create())。

-System Vのセマフォ(semadj)の中止値。

-リソースの制限。

-消費されたプロセッサー時間(times()から取得)。

-消費リソース(getrusage()から取得)。

-値nice(setpriority()およびnice()を使用して設定)。



以下は、個々のスレッドに固有の属性です。



-フロー識別子(セクション29.5を参照)。

-信号マスク。

-特定のストリームに関連するデータ(セクション31.3を参照)。

-代替シグナルスタック(sigaltstack())。

-変数errno。

-浮動小数点設定(env(3)を参照)。

-リアルタイムの計画ポリシーと優先度(セクション35.2および35.3を参照)。

-CPUバインディング(Linuxにのみ適用、セクション35.4で説明)。

-機能(Linuxにのみ適用、39章で説明)。

-スタック(ローカル変数と関数呼び出しのレイアウト情報)。



図に見られるように 29.1、個々のスレッドに関連するすべてのスタックは同じ仮想アドレス空間内にあります。 これは、適切なポインターを持つスレッドが、相互のスタックを介してデータを交換できることを意味します。 これは便利な場合もありますが、ローカル変数が置かれているスタックの有効期間中のみ有効であるという事実から生じる依存関係を解決するコードを記述する際には注意が必要です(関数が値を返す場合、そのスタックで使用されるメモリの一部は次の関数呼び出し中に再利用されます;スレッドが終了すると、そのスタックが正式に配置されていたメモリ部分が別のスレッドで利用可能になります)。 この依存関係の不適切な処理は、追跡が困難なエラーにつながる可能性があります。



29.2。 Pthreadsプログラミングインターフェイスの概要



1980年代後半から1990年代初頭には、ストリームを操作するためのプログラミングインターフェイスがいくつかありました。 1995年に、POSIX.1 APIはPOSIXスレッドAPIを記述しました。これは後にSUSv3の一部になりました。 Pthreadsプログラミングインターフェイスは、いくつかの概念に基づいています。 実装について詳しく調べながら、それらについて詳しく説明します。



Pthreadsのデータ型

Pthreadsプログラミングインターフェイスは、いくつかのデータ型を定義します。その一部を表にリストします。 29.1。 それらのほとんどについては、次のページで説明します。



画像






SUSv3標準には、これらのデータ型の表現方法に関する詳細が含まれていないため、ポータブルアプリケーションでは、これらのデータ型を不透明と見なす必要があります。 これは、プログラムがこれらのタイプの変数の構造または内容に依存してはならないことを意味します。 特に、==演算子を使用してこのような変数を比較することはできません。



ストリームとerrno変数

従来のUNIXプログラミングインターフェイスでは、errno変数はグローバルで整数です。 ただし、これはマルチスレッドプログラムには十分ではありません。 スレッドがerrnoグローバル変数にエラーを書き込んだ関数を呼び出した場合、これは関数を呼び出してerrnoの値をチェックする他のスレッドに誤解を招く可能性があります。 つまり、結果は競合状態になります。 したがって、マルチスレッドプログラムでは、各スレッドにerrnoの独自のインスタンスがあります。 Linux(およびほとんどのUNIX実装)では、これにおよそ1つのアプローチが使用されます:errnoはマクロとして宣言され、個々のスレッドに固有のlvalue形式の可変値を返す関数呼び出しに展開されます(lvalueの値は変更のためにアクセスできるため、前と同様に、マルチスレッドプログラムにerrno = valueの形式の割り当て操作を記述できます。



上記の要約:エラー報告手順がUNIXプログラミングインターフェイスで使用される従来のアプローチと完全に一致するように、errnoメカニズムがスレッドに統合されました。



Pthreadsの関数によって返される値

従来、システムコールと一部の関数は、成功すると0を返し、エラーが発生すると–1を返します。 エラー自体を示すために、errno変数が使用されます。 Pthreadsプログラミングインターフェイスの関数の動作は異なります。 成功した場合は0も返されますが、エラーがある場合は正の値が使用されます。 これは、従来のUNIXシステムコールでerrnoに割り当てることができる値の1つです。



マルチスレッドプログラムのerrnoへの各リンクは、関数の呼び出しに関連するオーバーヘッドを運ぶため、プログラムは、Pthreadsから関数によって返される値をこの変数に直接割り当てません。 代わりに、以下に示すように、中間変数と診断関数errExitEN()(サブセクション3.5.2を参照)を使用します。



pthread_t *thread; int s; s = pthread_create(&thread, NULL, func, &arg); if (s != 0) errExitEN(s, "pthread_create");
      
      





Pthreadsのコンパイル

Linuxでは、Pthreadsプログラミングインターフェイスを使用するプログラムは、cc -pthreadパラメーターを使用してコンパイルする必要があります。 このパラメーターのアクションの中で、次のものを区別できます。





マルチスレッドプログラムの特定のコンパイルオプションは、実装(およびコンパイラ)によって異なります。 一部のシステム(Tru64など)もcc-pthreadを使用しますが、cc -mtはSolarisおよびHP-UXで使用されます。



29.3。 スレッド作成



プログラムを開始した直後の最終プロセスは、ソースまたはメインと呼ばれる1つのスレッドで構成されます。 このセクションでは、追加のスレッドを作成する方法を学習します。



pthread_create()関数は、新しいスレッドを作成するために使用されます。



画像






新しいスレッドは、開始値として指定された関数を呼び出して引数引数(つまり、start(arg))を受け入れることで実行を開始します。 pthread_create()を呼び出したスレッドは、この呼び出しに続くステートメントを実行することにより実行を継続します(これは、セクション28.2で説明した、glibcライブラリからのclone()システムコールのラッパー関数の動作に対応します)。



arg引数はvoid *として宣言されます。 これは、任意のタイプのオブジェクトへのポインターを開始関数に渡すことができることを意味します。 通常、グローバルスペースまたはヒープにある変数を指しますが、NULL値を使用することもできます。 start関数にいくつかの引数を渡す必要がある場合、これらの引数を個別のフィールドとして含む構造体へのポインタをargとして提供できます。 適切なキャストを使用して、argを整数(int)として指定することもできます。



厳密に言えば、C言語の標準では、intをvoid *にキャストした結果は記述されていません。 ただし、ほとんどのコンパイラはこの操作を許可し、予測可能な結果、つまりint j ==(int)((void *)j)を生成します。



start関数によって返される値もvoid *型であり、arg引数として解釈できます。 以下では、pthread_join()関数を見ると、この値がどのように使用されるかがわかります。



初期ストリーム関数によって返された値を整数にキャストするときは注意が必要です。 実際、ストリームがキャンセルされたときに返されるPTHREAD_CANCELED値(第32章を参照)は、通常、void *への整数キャストとして実装されます。 初期関数がこの値を返す場合、pthread_join()を実行している別のスレッドは、これをスレッドキャンセル通知として誤って受け入れます。 スレッドのキャンセルを許可し、初期関数から返される値として整数を使用するアプリケーションでは、スレッドが正常に終了するときに、これらの値がPTHREAD_CANCELED定数と一致しないことを確認する必要があります(現在の実装でPthreads)。 ポータブルアプリケーションも同じように動作しますが、動作可能なすべての実装が必要です。



スレッド引数は、pthread_t型のバッファを指し、pthread_create()は、関数を返す前に、作成されたスレッドの一意の識別子を書き込みます。 この識別子は、Pthreadへのさらなる呼び出しでこのスレッドを参照するために使用できます。



SUSv3標準では、スレッドが指すバッファは、新しいスレッドを開始する前に初期化する必要がないと明確に規定されています。 つまり、pthread_create()関数が戻る前に、新しいスレッドが動作を開始できます。 新しいスレッドが独自の識別子を取得する必要がある場合、このためにpthread_self()関数(セクション29.5で説明)を使用する必要があります。



attr引数は、新しいスレッドのさまざまな属性を含むpthread_attr_tオブジェクトへのポインターです(セクション29.8に戻ります)。 attrをNULLに設定すると、デフォルトの属性でストリームが作成されます。これは、本書のほとんどの例で行うことです。



プログラムは、pthread_create()を呼び出した後、スケジューラーがプロセッサー時間を割り当てるスレッドを認識しません(マルチプロセッサーシステムでは、両方のスレッドを異なるCPUで同時に実行できます)。 特定の計画手順に明示的に依存するプログラムは、24.4項で説明したのと同じ種類の競合状態の影響を受けます。 特定の実行順序を保証する必要がある場合は、第30章で説明した同期方法のいずれかを使用する必要があります。



29.4。 スレッド補完



スレッドの実行は、次のいずれかの理由で終了します。





pthread_exit()関数は、呼び出しスレッドを終了し、pthread_join()関数を使用して別のスレッドから取得できる戻り値を示します。



 include <pthread.h> void pthread_exit(void *retval);
      
      





pthread_exit()の呼び出しは、スレッドの初期関数内でreturnステートメントを実行することと同等です。ただし、pthread_exit()は、初期関数によって起動されたコードから呼び出すことができます。



retval引数には、ストリームによって返される値が保持されます。 retvalが指す値は、pthread_exit()の呼び出しの終了時にその内容が未定義になるため、スレッド自体のスタック上にあるべきではありません(プロセスの仮想メモリのこの部分は、新しいスレッドのスタックにすぐに割り当てられます)。 初期関数のreturnステートメントで渡される値についても同じことが言えます。



メインスレッドがexit()またはreturnステートメントの代わりにpthread_exit()を呼び出した場合、残りのスレッドは引き続き実行されます。



29.5。 ストリーム識別子



プロセス内の各スレッドには、独自の一意の識別子があります。 pthread_create()によって呼び出しスレッドに返されます。 さらに、pthread_self()関数を使用して、スレッドは独自の識別子を取得できます。



 include <pthread.h> pthread_t pthread_self(void);
      
      





呼び出しスレッドの識別子を返します



アプリケーション内のフロー識別子は、次のように使用できます。





pthread_equal()関数を使用すると、2つのスレッド識別子のIDを確認できます。



画像






たとえば、呼び出しスレッドの識別子がtid変数に格納されている値と一致するかどうかを確認するには、次のコードを記述できます。



 if (pthread_equal(tid, pthread_self()) printf("tid matches self\n");
      
      





pthread_tデータ型は不透明として認識される必要があるため、pthread_equal()関数の必要性が生じます。 Linuxでは、unsigned long型ですが、他のシステムでは、ポインターまたは構造体にすることができます。



NPTLライブラリでは、pthread_tは実際にはunsigned longにキャストされるポインターです。



SUSv3標準では、pthread_t型をスカラーにする必要はありません。 構造物である可能性があります。 したがって、上記のストリーム識別子を出力するためのコードは移植性がありません(ただし、Linuxを含む多くのシステムで機能し、デバッグ中に役立ちます)。



 pthread_t thr; printf("Thread ID = %ld\n", (long) thr); /* ! */
      
      





Linuxでは、スレッド識別子はすべてのプロセスに固有です。 ただし、他のシステムではそうではない場合があります。 SUSv3標準では、特に、ポータブルアプリケーションはこれらの識別子に依存して他のプロセスのスレッドを識別することはできないと規定しています。 また、pthread_join()関数を使用して完了したスレッドを接続した後、または切断されたスレッドを完了した後、ストリームライブラリがこれらの識別子を再利用できることを示します(pthread_join()関数については次のセクションで、切断されたスレッドはセクション29.7で説明します)。



gettid()システムコール(Linuxでのみ利用可能)によって返されるPOSIXと通常のストリームの識別子は同じではありません。 POSIXスレッド識別子は、スレッドライブラリの実装によって割り当てられ、維持されます。 通常のスレッド識別子はgettid()を呼び出すことで返され、カーネルによって割り当てられた番号(プロセス識別子に類似)です。 NPTLライブラリはカーネルによって発行された一意のスレッド識別子を使用しますが、アプリケーションはそれらについて知る必要がないことがよくあります(さらに、それらを操作すると、異なるシステム間の移植性がアプリケーションから奪われます)。



29.6。 完了したスレッドに参加する



pthread_join()関数は、thread引数で指定されたスレッドが終了するまで待機します(スレッドが既に完了している場合は、すぐに戻ります)。 この操作は参加と呼ばれます。



画像






retval引数がゼロ以外のポインターである場合、関数は、完了したスレッドの戻り値のコピー、つまり、スレッドがreturnステートメントを実行したとき、またはpthread_exit()を呼び出して指定された値のコピーを受け取ります。



すでに接続されているスレッドの識別子に対してpthread_join()関数を呼び出すと、予期しない結果が生じる可能性があります。 たとえば、後で作成されたスレッドに参加して、同じ識別子を再利用できます。



スレッドが切断されていない場合(セクション29.7を参照)、pthread_join()関数を使用してスレッドを結合する必要があります。 これに失敗すると、完成したストリームは「ゾンビ」プロセスに類似したものになります(セクション26.2を参照)。 リソースの浪費に加えて、これは新しいスレッドを作成できなくなるという事実につながる可能性があります(十分な「ゾンビ」フローが蓄積した場合)。



pthread_join()がスレッド上で実行する手順は、プロセスのコンテキストでのwaitpid()呼び出しに似ています。 しかし、それらの間には顕著な違いがあります。



retval引数がゼロ以外のポインターである場合、関数は、完了したスレッドの戻り値のコピー、つまり、スレッドがreturnステートメントを実行したとき、またはpthread_exit()を呼び出して指定された値のコピーを受け取ります。



すでに接続されているスレッドの識別子に対してpthread_join()関数を呼び出すと、予期しない結果が生じる可能性があります。 たとえば、後で作成されたスレッドに参加して、同じ識別子を再利用できます。



スレッドが切断されていない場合(セクション29.7を参照)、pthread_join()関数を使用してスレッドを結合する必要があります。 これに失敗すると、完成したストリームは「ゾンビ」プロセスに類似したものになります(セクション26.2を参照)。 リソースの浪費に加えて、これは新しいスレッドを作成できなくなるという事実につながる可能性があります(十分な「ゾンビ」フローが蓄積した場合)。



pthread_join()がスレッド上で実行する手順は、プロセスのコンテキストでのwaitpid()呼び出しに似ています。 しかし、それらの間には顕著な違いがあります。





pthread_join()関数が特定の識別子を持つスレッドのみを接続できるという制限は、意図的に作成されました。 アイデアは、プログラムが知っているスレッドのみに参加するというものです。 「任意のストリームに結合する」操作の問題は、ストリームに階層がないという事実から発生するため、この方法で、ライブラリ関数(セクション30.2で説明した条件変数を使用して作成された.4、既知のストリームのみを結合できます)。 その結果、ライブラリはそのステータスを取得するためにこのストリームに参加できなくなり、すでに接続されているストリームに参加しようとするとエラーが発生します。 言い換えると、「任意のストリームへの結合」操作は、アプリケーションのモジュール式アーキテクチャと互換性がありません。



プログラム例

リスト29.1に示すプログラムは、新しいスレッドを作成して結合します。



リスト29.1 Pthreadsライブラリを使用した簡単なプログラム



 ______________________________________________________________threads/simple_thread c #include <pthread.h> #include "tlpi_hdr.h" static void * threadFunc(void *arg) { char *s = (char *) arg; printf("%s", s); return (void *) strlen(s); } int main(int argc, char *argv[]) { pthread_t t1; void *res; int s; s = pthread_create(&t1, NULL, threadFunc, "Hello world\n"); if (s != 0) errExitEN(s, "pthread_create"); printf("Message from main()\n"); s = pthread_join(t1, &res); if (s != 0) errExitEN(s, "pthread_join"); printf("Thread returned %ld\n", (long) res); exit(EXIT_SUCCESS); } ______________________________________________________________threads/simple_thread.c
      
      





このプログラムを実行すると、次のように表示されます。



 $ ./simple_thread Message from main() Hello world Thread returned 12
      
      





最初の2行の出力順序は、スケジューラが2つのスレッドを管理する方法によって異なります。



29.7。 ストリームを切断する



デフォルトでは、ストリームは結合可能です。 これは、完了すると、pthread_join()関数を使用して別のスレッドからステータスを取得できることを意味します。 ストリームによって返されるステータスは重要ではない場合があります。 システムがリソースを自動的に解放し、スレッドが終了したらスレッドを削除する必要があります。 この場合、pthread_detach()関数を使用してスレッド引数にスレッドIDを指定することにより、スレッドを切断済みとしてマークできます。



画像






pthread_detach()関数の使用例は、スレッドがそれ自体を切断する次の呼び出しです。



 pthread_detach(pthread_self());
      
      





スレッドが既に切断されている場合、pthread_join()関数を使用して戻りステータスを取得することはできません。 また、再びドッキング可能にすることもできません。



切断されたスレッドは、別のスレッドで行われたexit()呼び出しや、メインプログラムで実行されたreturnステートメントに対して耐性を持ちません。 これらの状況のいずれにおいても、接続されているかどうかに関係なく、プロセス内のすべてのスレッドはすぐに終了します。 言い換えると、pthread_detach()関数は、スレッドが終了した後の動作に責任を負いますが、スレッドが終了した状況には責任を負いません。



29.8。 ストリーム属性



タイプpthread_attr_tのpthread_create()関数のattr引数を使用して、新しいスレッドを作成するときに使用される属性を指定できることは既に述べました。 これらの属性(詳細については、この章の最後にリストされているリンクを参照)の検討や、pthread_attr_tオブジェクトを操作できるPthreadのさまざまな関数のプロトタイプの調査は行いません。 これらの属性には、ストリームスタックの場所とサイズ、そのスケジューリングポリシーと優先度(セクション35.2と35.3で説明されているリアルタイムスケジューリングポリシーとプロセスの優先度に類似)などの情報、およびストリームが接続可能か切断済みか。



これらの属性の使用例をリスト29.2に示します。この例では、スレッドが作成され、そのスレッドはその出現時に切断されます(その後のpthread_detach()の呼び出しの結果としてではありません)。 最初に、このコードはデフォルト値を使用して属性で構造を初期化し、切断されたストリームを作成するために必要な属性を設定し、この構造を使用して新しいストリームを作成します。 作成手順の最後に、属性を持つオブジェクトは不要として削除されます。



リスト29.2。 切断属性を持つスレッドの作成



 __________________________________________________   threads/detached_attrib.c #include <pthread.h> #include "tlpi_hdr.h" static void * threadFunc(void *x) { return x; } int main(int argc, char *argv[]) { pthread_t thr; pthread_attr_t attr; int s; s = pthread_attr_init(&attr); /*     */ if (s != 0) errExitEN(s, "pthread_attr_init"); s = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); if (s != 0) errExitEN(s, "pthread_attr_setdetachstate"); s = pthread_create(&thr, &attr, threadFunc, (void *) 1); if (s != 0) errExitEN(s, "pthread_create"); s = pthread_attr_destroy(&attr); /*    */ if (s != 0) errExitEN(s, "pthread_attr_destroy"); s = pthread_join(thr, NULL); if (s != 0) errExitEN(s, "pthread_join failed as expected"); exit(EXIT_SUCCESS); } __________________________________________________   threads/detached_attrib.c
      
      





29.9。 スレッドとプロセスの比較



このセクションでは、アプリケーションの基盤としてスレッドとプロセスを選択する際に考慮すべきいくつかの要因について簡単に説明します。 まず、マルチスレッドアプローチの利点について説明します。





以下は、スレッドとプロセスの選択に影響する可能性のある追加のポイントです。





29.10. まとめ



マルチスレッドプロセスでは、同じプログラムで異なるスレッドが同時に実行されます。すべてに共通のグローバル変数と束がありますが、それぞれにローカル変数用の独自の個別のスタックがあります。同じプロセスの異なるスレッドも、プロセス識別子、開いているファイル記述子、シグナルアクション、現在のディレクトリ、リソース制限など、多くの属性を共有します。



スレッドの重要な機能は、プロセスと比較して情報の交換が簡単なことです。このため、一部のソフトウェアアーキテクチャは、マルチプロセスアプローチよりもマルチスレッドアプローチのほうが優れています。さらに、状況によっては、スレッドのパフォーマンスが向上する場合があります(たとえば、スレッドはプロセスよりも速く作成されます)が、この要素は通常、スレッドとプロセスを選択する場合に二次的です。



pthread_create(). , pthread_exit() ( exit() , ). (, pthread_detach()), pthread_join(), .



»

»

»



20% — Linux



All Articles