Linux 学習記録 - 이십사 マルチスレッド (1)


1. Linuxの観点から理解する

プロセスを作成するとき、PCB 構造が存在します。CPU には多くのレジスタがあり、さまざまな種類のデータを保存します。たとえば、そのうちの 1 つはプロセス PCB を指します。PCB が変更される限り、指すプロセスは変わりません。違う。プロセスを再度作成するときに、以前に子プロセスを作成したときのようなアドレス空間やその他の操作ではなく、PCB のみが作成される場合、これらの新しい PCB は元の PCB と同じアドレス空間を指すため、次のように数行書くことができます。コードに異なる関数が含まれると、各 PCB はアドレス空間のコード領域にアクセスして異なるコードを実行できます。CPU が指す PCB に関係なく、その関数が実行されます。このようにプロセスで作成される複数の基板をスレッドと呼び、このように実行する方法が同時実行です。

スレッドはプロセスの一部を実行するため、スレッドの実行粒度はプロセスの実行粒度よりも細かくなります。CPU の場合、PCB のみを参照するため、スレッドなのかプロセスなのかは不明です。スレッドはプロセス内の実行の流れです。スレッドの切り替えでは PCB を切り替えるだけでよく、アドレス空間やページ テーブルを切り替える必要はありません。CPU はスレッドかどうかを知らないため、切り替え時にアドレス空間を変更するかどうかをどのように判断するのでしょうか? 実際、切り替えの作業はオペレーティング システムによって行われ、CPU が切り替えのためのシステムのコードを実行します。

CPUには高速キャッシュであるハードウェアキャッシュが搭載されています。プロセスの実行中に 100 行のコードにアクセスすると、101 行にもアクセスする可能性があるため、速度を上げるために、キャッシュはまず 100 行付近のデータをキャッシュします。スレッドを切り替える場合、すべて同じプロセスであるためキャッシュ データを変更する必要はありませんが、異なるアドレス空間を持つプロセスを切り替える場合は、キャッシュ データを無効にしてクリアし、新しいスレッドのデータをキャッシュする必要があります。プロセス。したがって、スレッドのスケジューリングコストが低くなります。

私たちが現在知っているスレッドは実際には実行フローであるはずであり、複数の実行フロー、アドレス空間、ページ テーブルを含む部分全体がプロセスです。プロセスは、システム リソースの割り当てを担当する基本的なエンティティです。プロセス内、つまりプロセス構造内に複数の task_struct が存在する場合があります。CPUから見るとPCBしか認識しないので、スレッドは CPU スケジューリングの基本単位です。

2. すべてのオペレーティング システムがこのように管理されるわけではありません

スレッドの数はプロセスの数よりも多くなければならないことがわかります. オペレーティング システムはスレッドを管理する必要があります. 同様に、スレッドを最初に記述してから整理する必要があります. 一部のオペレーティング システムにはスレッド構造 (TCB) があります。スレッド制御ブロック。TCB は PCB に属しており、この 2 つの関係を維持する必要があるほか、プロセスやスレッドのスケジューリングも必要です。オペレーティング システムがこのように設計されている場合、システムは非常に複雑になりますが、Windows はこれを実行しており、そのカーネルは本当のスレッド。

Linux はそのように設計されていません。実際のアプリケーションでは、スケジューリング スレッドとスケジューリング プロセスには多くの類似点があることがわかります。そのため、Linux の TCB は PCB を直接コピーし、コード内で PCB の構造を再利用し、その PCB を使用して Linux の TCB をシミュレートします。スレッドは、プロセス スキームによってシミュレートされたスレッドであり、Linux システムを常にシャットダウンできないことも決定しますが、Windows が常にシャットダウンしていない場合は、終わる。

コードと構造を再利用することにより、スレッドの実行が容易になり、保守が容易になり、より効率的かつ安全になるため、Linux は常に中断することなく実行できます。システムでは、システム自体以外に最も頻繁に使用される機能はプロセスです。

Linux のスケジューリング実行フローは軽量プロセスと呼ばれます。スレッド関数を作成する場合、以下にpthread_createを記述します。

ここに画像の説明を挿入

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>

void* thread1_run(void* args)
{
    
    
    while(1)
    {
    
    
        printf("我是线程1, 我正在运行\n");
        sleep(1);
    }
}

void* thread2_run(void* args)
{
    
    
    while(1)
    {
    
    
        printf("我是线程2, 我正在运行\n");
        sleep(1);
    }
}

void* thread3_run(void* args)
{
    
    
    while(1)
    {
    
    
        printf("我是线程3, 我正在运行\n");
        sleep(1);
    }
}

int main()
{
    
    
    pthread_t t1, t2, t3;
    pthread_create(&t1, NULL, thread1_run, NULL);
    pthread_create(&t2, NULL, thread2_run, NULL);
    pthread_create(&t3, NULL, thread3_run, NULL);
    while(1)
    {
    
    
        printf("我是主线程,我正在运行\n");
        sleep(1);
    }
}

ps -aL | head -1 && ps -aL | grep 実行可能プログラム名を使用してスレッドを表示できます。LWP は軽量プロセス ID ですが、PID と同じなので、システムはスレッドをどのように区別しますか?二?システムはスケジュールを設定するときに LWP を確認しますが、スレッドが 1 つしかない場合は PID を確認するのが自然です。切り替える PID が現在の LWP と異なる場合、これはクロスプロセス スケジューリングとなり、アドレス空間とページ テーブルを切り替える必要があります。同様にプロセス内スケジューリングも行われます。

3. ページテーブルと物理メモリ

仮想アドレス空間の基本単位はバイトなので、32 ビット マシンの仮想アドレス空間は 4G ですが、アドレスはいくつありますか? 4G は 2^32 バイトです。仮想から物理への変換にはページ テーブルが必要です。ページ テーブルは一種のソフトウェアであり、一定量のメモリを占有する必要があります。ページ テーブルの大きさは 2^32 バイトの仮想空間を維持する必要があり、対応する物理メモリは kv を維持する必要があります。ページテーブルにはその他の属性もあります アドレスは4バイトなので、仮想と物理で8バイト ファイル属性の列も4バイトで合計12バイトとすると、2バイトとなります^ 32 * 12、48G! 出来ますか?それは不可能なので、この点は間違っています。

物理メモリのアドレス指定単位は 1 バイトであり、バイト単位でメモリにアクセスしたり、ビットを使用したりすることもできますが、物理メモリはバイト単位で配置されているわけではありません。物理メモリは頻繁にディスクと対話し、ディスクの速度は比較的遅いため、物理メモリがバイト単位でデータにアクセスする場合、効率は高くありません。1 回の IO で大量のデータが移動される場合、効率は高くありません。少量のデータを移動するには、複数の IO よりも効率的です。したがって、システムがディスクなどのデバイスと対話するときは、ブロックを単位として使用する必要があります。前に書いたファイル システムによると、この単位は 4kb、8 セクターです。もちろん、システムが異なれば異なります。少し変えたい場合はどうしますか?これも IO4kb である必要があります。IO4kbが必要なため、ファイルなどのデータもディスクに格納する際は4kb単位で格納され、例えば9kbのデータは4kbが3つとなり、IO時にアクセスする領域が大きなブロックで見つけられるようになり、頭と尻尾を見つけて返すことができます。ファイルには動的ライブラリと静的ライブラリ、および実行可能プログラムが含まれます。これらも 4kb 単位に切り上げられて保存されます。

ディスクは、物理メモリやオペレーティング システムと同様に、この方法で管理されます。実際にメモリを管理する場合、メモリは4kbに分割されているため、メモリ管理の本質は、ディスク内の特定の4kbブロック(データ内容)を物理メモリの4kb空間(データ保存空間)に置くことです。物理メモリ内のこの 4kb コンテンツはページと呼ばれ、このスペースはページ フレームと呼ばれ、ディスク内の各 4kb スペースはページ フレームと呼ばれます。オペレーティング システムは物理メモリのページも管理します。最初に管理し、次に整理すると、ページのさまざまな属性を含むページ構造が作成されます。ページの編成には、システムは配列を使用します。この配列はバイトに基づいており、1048576 個の配列要素があります。指定されたメモリが使用されていないことが判明した場合、メモリ申請時に1に設定され、申請が実行されます。オペレーティング システムの管理方法には他にも、LRU やパートナー システムなどがあります。

どう見ても1回のIOで4kbのデータを送信するのに、1バイトしかアクセスせずに4kb送信するのはちょっともったいないでしょうか?実際には、オペレーティング システムは 4kb を転送するだけではありません。このシステムには、現代のコンピューターがデータをプリロードするための理論的基礎である局所性の原則があり、アクセスしているデータの隣接または近くのデータをプリロードするため、データの近くを事前に記録することで将来の IO が削減されます。アクセスされる頻度。

最初の質問に戻りますが、ページ テーブルは本当に 48GB ですか? ページテーブルはこのようにはなりません。仮想アドレスは 32 ビットで、ページ テーブルに渡されるときに全体として使用されるのではなく、10+10+12 に分割されます。システムは最初の 10 ビットをキーとして使用してページ テーブル内の値を検索するため、ページ テーブルには 2 ^ 10 行が含まれ、対応する 2 次ページ テーブルが検索されます。さらに 10 ビットを取得して 2 次ページ テーブルに進みます。ページ テーブル レベルレベルのページ テーブルの検索、第 2 レベルのページ テーブルは最大 2 ^ 10 の正方形の行です。このとき検索される値はページ フレームのアドレスです。管理ページに書き込まれたばかりの配列は 1048576 です。特定の要素をそれぞれ数えた第 2 レベルのページ テーブル内の値は、物理メモリのページ フレームの開始アドレスに対応する 20 ビットである必要があり、最後の 12 ビットはページ内のオフセットであり、ページ フレームの開始アドレス + ページ内のオフセットによって検出されます。 任意の開始バイト アドレス。

12 ビットには 4095 個のデータがあるため、IO の基本単位は 4kb です。オペレーティング システムがメモリの任意のバイトを見つける方法は、ベース アドレス + オフセットです。

この管理により、ページテーブルは最大 2 ^ 10 個の 1M のデータを持ち、各アドレスは 4 バイトなので最大 4M になります。ユーザーがすべての物理メモリにアクセスすることは不可能であり、ページ テーブルは使用されたときにのみ作成され、使用された分だけ作成されるため、実際のページ テーブルは小さくなります。

先ほど書き込んだ第 1 レベルのページテーブルをページディレクトリ、第 2 レベルのページテーブルをページテーブルエントリと呼び、呼び名が異なりますが、すべて同じものです。

実際に言語を使う場合、int型の変数を複数定義したり、配列やクラスオブジェクトなどを定義したりするなど、1バイトだけのビットや変数に直接アクセスすることはほとんどありませんが、1バイトからアクセスするということでしょうか?どのオブジェクトまたは変数にも複数のバイトが含まれる場合がありますが、アドレスをフェッチするときに取得されるアドレス番号は常に 1 つだけです。このアドレスは開始アドレスに型を加えたものです。つまり、オフセットはアドレス全体です。

実際に malloc メモリを適用する場合、システムは仮想アドレス空間にのみ適用しますが、ユーザーが実際にアクセスすると、システムは自動的に特定の物理メモリを適用するかページ テーブルを埋めます。システムはどのようにしてページ テーブルに自動的にデータを入力しますか? ページフォルトにより中断されました。変換が開始されようとすると、MMU は現時点ではページ テーブルが存在しないことを検出し、割り込み信号をレジスタに送信し、システムはデフォルトのアクションを実行してページ テーブルを作成し、物理メモリを適用します。 。ディスクデータにアクセスする場合も同様に、ファイルの一部が最初にロードされ、実際にアクセスするときに引き続きロードされます。

ページ テーブルと物理メモリに関する操作実行フローはこれらを考慮せず、ページ テーブル、ページ フォールト割り込みなどを通じて結合されます。

ページ テーブルはそれ以上で、ページ テーブル エントリには他の属性もあります。たとえば、ヒットがアクセス対象のデータが物理メモリにすでに存在するかどうかを表すかどうか、RWX がデータへのアクセス許可を表すかどうかなどです。

前のコードでは、 char* の内容は変更できず、文字定数領域に格納され、読み取りのみが許可されています。s は指定された文字列の仮想開始アドレスを格納するため、*s はアドレス指定時に仮想から物理に変換されるため、ページ テーブルを確認する必要があり、操作の権限を確認してこの操作を見つけます。不正な場合は、 MMU には例外が発生します。システムが例外を認識した後、例外はシグナルに変換され、シグナルはターゲット プロセスに送信されます。プロセスがカーネル状態からユーザー状態に変化するときに、シグナルが処理されます。となり、プロセスは終了します。

4. スレッドの長所と短所

利点:
1. 新しいスレッドを作成するコストは、新しいプロセスを作成するよりもはるかに小さいです。
2. プロセス間の切り替えと比較して、スレッド間の切り替えに必要なオペレーティング システムの作業ははるかに少なくなり
ます。 3. スレッドが占有するリソースは、プロセス間の切り替えに比べてはるかに少なくなります。プロセス
4. マルチプロセッサの並列数を最大限に活用できます
5. 遅い I/O 操作の終了を待っている間、プログラムは他の計算タスクを実行できます
サーバー システム上で実行され、計算を複数に分解します実現するスレッド (暗号化と復号化、ファイルの圧縮と解凍などはアルゴリズムに関係しますが、スレッド数は適切である必要があり、プロセス/スレッドと CPU/コアの数は同じである必要があります) 8. I
/ O 集約型のアプリケーションは、パフォーマンスを向上させるために、I/O 操作をオーバーラップさせます。スレッドは、さまざまな I/O 操作を同時に待機できます (ダウンロード、アップロード、および IO は主に IO リソース、ディスク IO、およびネットワーク帯域幅を消費します。ここで、スレッドが多いほど、より多くのことが可能になります)。

欠点:
1. パフォーマンスの損失
外部イベントによってめったにブロックされない計算集約型のスレッドは、多くの場合、同じプロセッサを他のスレッドと共有できません。計算負荷の高いスレッドの数が利用可能なプロセッサよりも大きい場合、大幅なパフォーマンスの損失が発生する可能性があります。ここでのパフォーマンスの損失とは、利用可能なリソースが変化しないまま、追加の同期とスケジューリングのオーバーヘッドが追加されることを指します。
2. 堅牢性の低下
マルチスレッドで記述する場合は、より包括的かつ綿密な検討が必要となるため、マルチスレッドプログラムでは、時間配分のわずかなズレや、共有すべきでない変数の共有により悪影響が生じる可能性が高くなります。つまり、スレッド間の保護が欠如しています。
3. アクセス制御の欠如
プロセスはアクセス制御の基本的な粒度であり、1 つのスレッドで一部の OS 関数を呼び出すと、プロセス全体に影響します。
4. プログラミングの難易度の増加
マルチスレッド プログラムの作成とデバッグは、シングルスレッド プログラムよりもはるかに困難です。

スレッドを使用するには、gcc の最後に -lpthread を追加します。スレッド作成関数は pthread_create 、変数の型は pthread_t です。

マルチスレッド プログラムでは、1 つのスレッドがクラッシュするとプロセスがクラッシュします。システムの観点から見ると、スレッドはプロセスの実行ブランチです。スレッドが強制終了されると、プロセスも強制終了されます。信号の観点から見ると、ページ テーブルの変換時に MMU が書き込み許可を認識できない場合、システムは例外を取得し、プロセスがシグナルを送信します。そのシグナルはプロセスに基づいており、プロセスの各スレッドに例外が書き込まれるため、すべてのスレッドがハングアップします。

インプロセス実行フローで認識されるリソースはアドレス空間を通じて認識されるため、複数の LWP は同じアドレス空間を認識し、すべてのスレッドがデータの大部分を共有する可能性があります。

5. プロセスとスレッドの違い

スレッドには、スレッド ID、レジスタのセット、スタック、errno、シグナル マスク ワード、スケジューリング優先度などの独自のデータ部分もあります。ここで最も重要なことは、レジスタのセットと独立したスタック構造です。レジスタはスレッドの動的な切り替えに関連し、スタックはスレッド自体のデータに関連します。スレッドが共有するデータには、ファイル記述子テーブル、各シグナルの処理方法、カレントワーキングディレクトリ、ユーザーID、グループIDが含まれており、データ領域、コード領域、ヒープ領域をすべて共有することができます。

6. スレッドインターフェース

システムの観点から見ると、Linux には実際のスレッドはありませんが、プロセス シミュレートされたスレッド (LWP) があるため、Linux はスレッドを直接作成するためのシステム コールを提供せず、スレッドを作成するためのインターフェイスを提供するだけです。軽量プロセス。

ユーザーの観点から見ると、ユーザーはスレッドしか認識しないため、ライブラリは下位側で Linux インターフェイスをカプセル化し、上位側でスレッド制御用のインターフェイスをユーザーに提供します。この種のライブラリはユーザーレベルと呼ばれますスレッド ライブラリ。このライブラリは、pthread ライブラリとも呼ばれます。このライブラリは、どのシステムにも付属している必要があります。Linux では、このライブラリはネイティブ スレッド ライブラリと呼ばれます。

コードを書き始めましょう

1、pthread_create。

スレッドの作成は pthread_create です。

ここに画像の説明を挿入

最初のパラメータはスレッド ID (出力パラメータ)、attr はスレッド属性 (通常は NULL に設定されます)、3 番目のパラメータは関数ポインタ (新しいスレッドによって実行される関数とメソッド)、arg はこの関数のパラメータ。

スレッド インターフェイスを使用する場合は、メイクファイルの g++ の後に -lpthread を記述する必要があります。

threadtest:thread.cc
    g++ -o $@ $^ -lpthread
.PHONY:clean
clean:
    rm -f threadtest
#include <iostream>
#include <unistd.h>
#include <phread.h>

void* thread_run(void* args)
{
    
    
    while(true)
    {
    
    
        cout << "new thread running" << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    
    
    pthread_t t;
    pthread_create(&t, nullptr, thread_run, nullptr);
    while(true)
    {
    
    
        cout << "main thread running, new thread id: " << t << endl;
        sleep(1);
    }
}

正式に出力するときは、コマンド ps -aL | head -l && ps -aL | grep threadtest を使用すると、表示される LWP がプログラムによって出力された結果と異なることがわかりますが、なぜそれらが異なるのでしょうか?存在とは何か?

この質問に対する答えは以下に書きます。コードでは、新しいスレッドを作成します。実行順序は、実際の実行時にスケジューラによって決定されます。

スレッドのバッチを作成する場合

void* thread_run(void* args)
{
    
    
    char* name = (char*)args;
    while(true)
    {
    
    
        cout << "new thread running" << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    
    
    pthread_t tids[NUM];
    for(int i = 0; i < NUM; i++)
    {
    
    
        char tname[64];
        snprintf(tname, sizeof(tname), "thread-%d", i + 1);
        pthread_create(tids + i, nullptr, thread_run, tname);
    }
    while(true)
    {
    
    
        cout << "main thread running, new thread id: " << endl;
        sleep(1);
    } 
    return 0;   
}

tname は、同じメイン スレッドの一時領域を指す 10 個のスレッドに相当します。tname が取得するのは開始アドレスです。この配列は毎回上書きされるため、最終的には最後のアドレスのみが残ります。これを出力すると、 、10 番目が表示されます。スレッドによって出力されるものはすべて 10 番目です。すべてのスレッド名を出力するには

        char* tname = new char[64];
        snprintf(tname, 64, "thread-%d", i + 1);

新しい領域がヒープ上に開かれ、ループ内で作成されます。thread_run 関数で、nullptr を返す前に name を削除して、各スレッドが独自の小さな領域を持ち、使い果たされると削除されます。pthread_create 関数が呼び出される前に、すべてのパラメータがコピーされ、呼び出され、渡され、再度新規作成されてから、関数呼び出しが実行されます。各スレッドによって使用されるスペースは異なります。しかし、この書き方は一般的です。

メイン スレッドの背後に無限ループがない場合は、直接 0 を返します。メイン スレッドとサブスレッドは実行を継続します。0 を返す前に一定期間スリープして、10 個のサブスレッドすべてが実行されて 0 を返すことができます。 、その後、すべてのスレッドが終了します。メインスレッドが終了すると、プロセスが終了し、すべてのリソースデータが解放され、すべてのサブスレッドも終了します。サブスレッドが作成されると、メインスレッドは待機する必要があり、そうしないとゾンビのような現象が発生しにくくなります。プロセス。

2、pthread_join

ここに画像の説明を挿入

    for(int i = 0; i < NUM; i++)
    {
    
    
        pthread_join(tids[i], nullptr);
    }

メインスレッドは新しいスレッドを待つ必要があり、メインスレッドは新しいスレッドが終了した後にのみ終了できます。この間、メインスレッドは待機する必要があり、待機するために join が使用されます。この待機はデフォルトでブロックされます。2 番目のパラメータは出力パラメータで、新しいスレッドが終了した結果を取得します。成功した場合は 0 を返します。

3. スレッドの終了

スレッドを終了するにはさまざまな方法があります

1. スレッド関数が実行されます
2. 終了します。exit はスレッドの終了ではなくプロセスの終了を意味するため、どのスレッドが exit を呼び出すと、プロセス全体が終了します。
3、pthread_exit スレッド終了インターフェイス

ここに画像の説明を挿入

void* thread_run(void* args)
{
    
    
    char* name = (char*)args;
    while(true)
    {
    
    
        cout << "new thread running" << endl;
        sleep(1);
        break;
    }
    delete name;
    pthread_exit(nullptr);
    //return nullptr;
}

このインターフェイスのパラメータは、新しいスレッドの結果を受け取るために使用される jion の 2 番目のパラメータと同じ型です。新しいスレッドを作成した後、それに与えられたタスクがどの程度うまく完了したかを知り、結果を返す必要があります。pthread_exit の括弧内には (void*)1 を渡すこともできますし、thread_run 関数の型と pthread_exit 関数のパラメータの型に応じて return も使用でき、メインスレッドがそれを受け取り、受け取り場所はjoin インターフェイスが呼び出される場所、join の 2 番目のパラメータは void**retval、void* *retval はこのように理解されるべきであり、変数の型と入力変数のアドレスになります。

    delete name;
    pthread_exit(nullptr);
    //return nullptr;
}

int main()
{
    
    
    pthread_t tids[NUM];
    for(int i = 0; i < NUM; i++)
    {
    
    
        char* tname = new char[64];
        snprintf(tname, 64, "thread-%d", i + 1);
        pthread_create(tids + i, nullptr, thread_run, tname);
    }
    void* ret = nullptr;
    for(int i = 0; i < NUM; i++)
    {
    
    
        int n = pthread_join(tids[i], &ret);
        if(n != 0) cerr << "pthread_join error" << endl;
        cout << "thread quit: " << (uint64_t)ret << endl;
    }
    cout << "all thread quit..." << endl;

(void*)1 が返された場合は ret に 1 が格納されるので、最後に強制的に int にして出力します。ここでこれを 1 に設定すると、最終的に、各スレッドが終了時に 1 を出力することがわかります。

しかし、ここでは、返された番号を取得しただけであることがわかります。では、スレッドが異常であるかどうかをどのように判断すればよいでしょうか? 実際、例外が発生するとプロセス全体が終了するため、この問題について心配する必要はありません。

抜けるときは型がvoidなので色々な型を渡すことができるのでクラスも渡すことができ、見たい情報をクラスに入れることができます。

#include <iostream>
#include <unistd.h>
#include <string>
#include <phread.h>
#include <ctime>
using namespace std;

#define NUM 10

class ThreadData
{
    
    
public:
    ThreadData(const string& name, int id, time_t createTime):_name(name), _id(id), _createTime((uint64_t)createTime)
    {
    
    }
    ~ThreadData()
    {
    
    }
public:
    string _name;
    int _id;
    uint64_t _createTime;
}


void* thread_run(void* args)
{
    
    
    //char* name = (char*)args;
    ThreadData* td = static_cast<ThreadData*>(args);//强转类型
    while(true)
    {
    
    
        cout << "thread is running, name" << td->_name << "create time: " << td->_createTime << "index: " <<td->_id << endl;
        sleep(4);
        break;
    }
    delete td ;
    pthread_exit(td);
    //return nullptr;
}

int main()
{
    
    
    pthread_t tids[NUM];
    for(int i = 0; i < NUM; i++)
    {
    
    
        //char* tname = new char[64];
        char tname[64];
        snprintf(tname, 64, "thread-%d", i + 1);
        ThreadData* td = new ThreadData(tname, i + 1, time(nullptr));
        pthread_create(tids + i, nullptr, thread_run, td);
    }
    void* ret = nullptr;
    for(int i = 0; i < NUM; i++)
    {
    
    
        int n = pthread_join(tids[i], &ret);
        if(n != 0) cerr << "pthread_join error" << endl;
        cout << "thread quit: " << (uint64_t)ret << endl;
    }
    cout << "all thread quit..." << endl;
    /*while(true)
    {
    
    
        cout << "main thread running, new thread id: " << endl;
        sleep(1);
    }*/
    return 0;
}

前のプロセスが終了したときなどのデータをクラスに戻すこともできます。次のように、ステータス ビットを表す int 変数をクラスに配置し、enum を使用してステータス ビットを指定します。

    void* ret = nullptr;
    for(int i = 0; i < NUM; i++)
    {
    
    
        int n = pthread_join(tids[i], &ret);
        if(n != 0) cerr << "pthread_join error" << endl;
        //cout << "thread quit: " << (uint64_t)ret << endl;
        ThreadData* td = static_cast<ThreadData*>(ret);
        if(td->_status == OK)
        {
    
    
            cout << td->_name << endl;
        }
        delete td;
    }

終了中のスレッドをキャンセルする

ここに画像の説明を挿入

void* threadRun(void* args)
{
    
    
    const char* name = (const char*)args;//static_cast<const char*>,传过来args参数的时候实际上是一个指针,指向字符串常量的起始地址
    int cnt = 5;
    while(cnt)
    {
    
    
        cout << name << "is running: " << cnt-- << "obtain self id: " << pthread_self() << endl;//查看本线程的id
        sleep(1);
    }
    pthread_exit((void*)1);
    //PTHREAD_CANCELED == ((void*)-1)
}

int main()
{
    
    
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");//省去一个变量,直接传一个字符串
    sleep(1);
    pthread_cancel(tid);
    void* ret = nullptr;
    pthread_join(tid, &ret);//join必须做,防止僵尸进程的类似问题
    cout << "new thread exit: " << (int64_t)ret << "quit thread: " << tid << endl;
    return 0;
}

プロセスがキャンセルされた場合、戻り値は PTHREAD_CANCELED、つまり ((void*)-1) になります。

4.糸離れ

メイン スレッドは、新しいスレッドを待機するときにブロックする必要はありません。スレッド分離の方法を使用して、メイン スレッドに他の処理を実行させることも、メイン スレッド自体を分離することもできます。スレッドが分離された場合、再度結合することはできず、強制的に結合するとエラーが報告されます。スレッドには結合できるという属性が付いていますが、スレッドが切り離されると、この属性は消滅します。

ここに画像の説明を挿入

#include <iostream>
#include <unistd.h>
#include <string>
#include <cstdio>
#include <cstring>
#include <phread.h>
#include <ctime>
using namespace std;

void* threadRun(void* args)
{
    
    
    string name = static_cast<const char*>(args);
    int cnt = 5;
    while(cnt)
    {
    
    
        cout << name << " : " << cnt-- << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    
    
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
    pthread_detach(tid);
    int n = pthread_join(tid, nullptr);
    if(0 != n)
    {
    
    
        cerr << "error: " << n << " : " << strerror(n) << endl;
    }
    return 0;
}

エラーが出力されます。メインスレッドからデタッチせずに新しいスレッドからデタッチする場合は、最初に pthread_detach(pthread_self()) と書いて、最終的には正常に出力されることがわかりますが、メインスレッドの結合前に sleep(1) を実行すると、関数を実行すると、エラーが出力されて終了します。

スレッドの作成時には実行順序は決まりません。いくつかのスレッドが作成された後、メソッドは実行されず、メインスレッドはこの時点でコードの実行を継続します。その場合、特定のスレッドの対応するメソッドが実行を開始する前に結合される可能性があります。メインスレッドは一時停止されます。もちろん、参加する前に参加できるかどうかを判断する必要があり、その後、スレッド自体が分離されますが、現時点ではメインスレッドはこれを知りません。

このコードにはロックが含まれているため、今のところ心配する必要はありません。

仕上げる。

おすすめ

転載: blog.csdn.net/kongqizyd146/article/details/130581998