C++11 マルチスレッド プログラミング 2: マルチスレッド通信、スレッド同期、ロック

 

C++11 マルチスレッド プログラミング 1: マルチスレッドの概要

C++11 マルチスレッド プログラミング 2: マルチスレッド通信、スレッド同期、ロック

C++11 マルチスレッド プログラミング 3: ロック リソース管理と条件変数 

C/C++ の基礎、ブースト スレッドの作成、スレッドの同期 


2.0 概要

        スレッド同期は、データ保護のためのメカニズムです。保護されるデータは共有データです。共有データは、複数のスレッドによってアクセスされるリソースの一部、つまり同じメモリの一部です。スレッドが 2 つある場合、Aと B が同時にデータを書き込み、A は 100 を書き込み、B は 200 を書き込み、このときスレッド C はこのメモリを同時に読み取ります。すると、どのデータが読み取られるでしょうか? この時点では間違いなくエラーが発生します。3 つのスレッドがこのメモリを同時に使用するため、エラーが発生します。そのため、スレッドの同期は、複数のスレッドが同時に何かを実行していることを意味するのではなく、複数のスレッドが同時に実行していることを意味します。何かを実行する 問題が発生した場合は、複数のスレッドを順番に実行します。つまり、スレッドの同期では、スレッドの並列実行が許可されず、スレッドが直線的に実行されるため、データのセキュリティが保証されます。
 
        4 つのスレッド A、B、C、D があるとします。前のスレッド A がメモリ内の共有リソースにアクセスすると、スレッド A が共有リソースにアクセスするまで、他のスレッド B、C、D はこのメモリ上で動作できません。 B、C、D のうちの 3 つは、メモリ ブロックへのアクセスが完了するまでこのメモリにアクセスできます。残りの 2 つは、すべてのスレッドがこのメモリ ブロックでの操作を終了するまでブロックして待機し続ける必要があります。

例:

#include <iostream>
#include <thread>
//#include <mutex>
//Linux make:  g++ -o main main3.c -lpthread
using namespace std;
//static mutex mut;

int i=0;

void thread_1(int n){
    while(n--){
       //mut.lock();
        i++;
       //mut.unlock();
    }
}

int main(){
	//n越大,i在不加锁的情况下出错越大
    int n=100000;
    thread th1(thread_1,n);
    thread th2(thread_1,n);
    th1.join();
    th2.join();
    cout<< i << endl;
    return 0;
}

 

        このプログラムを複数回実行すると、異なる結果が出力されることがわかります。つまり、途中でエラーがなければ、出力されるデータは同じになるはずです。これで、2 つのスレッドが一致していることがわかります。同じメモリ空間を同時に処理する操作 (現在のメモリ空間はグローバル変数 i) により、エラーが発生しました。

        複数のスレッドが CPU タイム スライスをタイムシェアして再利用します。つまり、スレッド A が CPU のタイム スライスを取得する必要があります。タイム スライスを取得した人が実行されます。スレッド A が CPU のタイム スライスを取得すると、CPU はカウントを開始します。 . データはどこから来たのですか? このデータは物理メモリから読み取られます。物理メモリ内のデータは CPU のレジスタにロードされ、レジスタを通じて処理されます。通常、物理メモリと CPU の間にキャッシュがあり、通常は 3 次キャッシュです。 . 物理メモリ→3次キャッシュ→2次キャッシュ→1次キャッシュ→CPUレジスタという順番でデータが転送されますが、キャッシュとは高速化するためのもので、レジスタの処理速度は物理メモリの処理速度よりもはるかに速いのです。このようなキャッシュがあると、データの処理効率を向上させることができます。

同期的に

        複数のスレッドが共有リソースにアクセスするときにデータが混乱する問題を解決するには、スレッドの同期が必要です。一般的に使用されるスレッド同期方法には、ミューテックス ロック、読み取り/書き込みロック、条件変数、セマフォの 4 つがあります。いわゆる共有リソースは、複数のスレッドからアクセスされる変数であり、通常、グローバル データ領域変数またはヒープ領域変数です。これらの変数に対応する共有リソースはクリティカル リソースとも呼ばれます。

         ロックされた領域内の各スレッドは、この領域を単独で実行することしかできず、同時に実行することはできません。現在のスレッドがロックを解除しに行った後でのみ、他のスレッドがロックを解除できます。3 つがブロックされている場合は、それらがロックを取得します。掴むとブロックが解除されます


2.1 マルチスレッドの状態と切り替え処理の解析

スレッド状態の説明:

        初期化 (Init):スレッドが作成されています。(つまり、スレッド オブジェクトを作成し、コールバック関数を設定します。スレッドは初期化状態にあり、スレッドのメモリ空間などを初期化します。実際、この部分には多くのコード介入はありません。実際には初期化から準備完了までの処理です。時間がかかるため、後からスレッドプールを使って時間を短縮する処理を行います。各種メモリの準備ができたら準備完了となります。) Ready: という意味ではありません。
        準備完了リストにあり、CPU スケジューリングを待っています。
        実行中:スレッドは実行中です。CPU によってスケジュールされます。
        ブロック済み:スレッドはブロックされ、一時停止されています。ブロックされたステータスには、保留 (ロック、イベント、セマフォなどのブロック)、一時停止 (アクティブな保留)、遅延 (遅延ブロック)、保留時間 (ロック、イベント、セマフォ時間などの待機中のタイムアウトによる) が含まれます。ブロック状態は、CPU スケジューリングが存在しなくなり、リソースを無駄にすることなく CPU スケジューリングが放棄されることを意味します。
        終了:スレッドは終了し、親スレッドが制御ブロック リソースを再利用するのを待ちます。


2.2 競合状態とクリティカル セクションでミューテックス コードが導入される

競合状態:複数のスレッドが同時に共有データの読み取りと書き込みを行う
クリティカル セクション:共有データの読み取りと書き込みを行うコード フラグメントは、
競合状態戦略を回避し、クリティカル セクションを保護する必要があります。クリティカル セクションに同時に入ることができるのは 1 つのスレッドだけです。


2.0 の概要の問題に対処するには:

このとき、ミューテックス ロック (ミューテックスは相互排他ロック) を追加する必要があり、ヘッダー ファイルをインクルードする必要があります: #include <mutex> static
mutex mux; mux はミューテックス変数と呼ばれます。リソースがロックされると、他のスレッドはインラインで待機することと同じになります - ブロッキング
mux.lock はオペレーティング システム レベルでのロックであり、mux ミューテックスは 1 つだけです。スレッドがアクティブに処理を始めるときは、最初にロックを取得する必要があります。 CPU リソースがロック リソースを占有するため、スレッドは片側になります。

#include <iostream>
#include <thread>
#include <mutex>	//需要包含的头文件
//Linux make:  g++ -o main main3.c -lpthread
using namespace std;

static mutex mut;	//添加互斥锁变量

int i=0;

void thread_1(int n){
    while(n--){
       mut.lock();	//获取锁资源,如果没有获得,则阻塞等待
        i++;
       mut.unlock();//释放锁
    }
}

int main(){
	//n越大,i在不加锁的情况下出错越大
    int n=100000;
    thread th1(thread_1,n);
    thread th2(thread_1,n);
    th1.join();
    th2.join();
    cout<< i << endl;
    return 0;
}

 

複数回実行した結果から、エラーはなくなり、出力は毎回同じで、スレッドの同期は成功しているようです。


2.3 ミューテックスロックのピットスレッドがリソースを確保できない理由

理想的には、スレッドがロック リソースを解放した後、後続のスレッドがロック リソースを取得するためにキューに並ぶことになりますが、実際には、1 つのスレッドが常にリソースを占有し、他のスレッドがリソースを取得できないという状況が発生することがあります。列に並んでいます。

#include <thread>
#include <iostream>
#include <string>
#include <mutex>
//Linux -lpthread
using namespace std;
static mutex mux;
 
void ThreadMainMux(int i)
{
    for (;;)
    {
        mux.lock();
        cout << i << "[in]" << endl;
		this_thread::sleep_for(100ms);
        mux.unlock();
    }
}
int main(int argc, char* argv[])
{
    for (int i = 0; i < 3; i++)
    {
        thread th(ThreadMainMux, i + 1);
        th.detach();
    }
 
    getchar();
    return 0;
}

 

結果として、このスレッドが常に入り、すべてのスレッドが理想的に表示されるわけではないことがわかります。これは、次の理由により、望ましい結果ではありません。

        このコードでは、スレッド 1 がロック リソースを取得すると、スレッド 2 とスレッド 3 はブロック状態になります。スレッド 1 がロック解除されると、スレッド 2 とスレッド 3 には即座にロック リソースを取得するものが必要になりますが、スレッド 1 つまり、ロックが解除されると、ロックに戻ります、つまり、ロックが解除された後にロックを再度申請します このロックは、オペレーティング システムのカーネルによって判断されます ロック リソースが占有されているかどうか スレッド 1 のときもちろん、私たちのオペレーティング システムはリアルタイム オペレーティング システムではないため、メモリ リソースはすぐには解放されません。また、ロック解除とロックの間の時間はマイクロ秒レベルである可能性があり、CPU スケジューリングは、後でこのリソースを検出する必要があります。一定期間。スレッド 1 がロック解除後すぐにロックに入り、オペレーティング システムが応答する時間がない場合。オペレーティング システムは、スレッド 1 が再びロック リソースを取得したと認識します。実際にはそうではありませんが、次の理由によります。スレッド 1 のリソースには、ロックに再度入る前に解放する時間がなかったので、キューに入れずに再度入ったので、ここが穴であるため、オペレーティング システムに時間を与えるために、ロックの解除とロックの前に遅延が追加されます。リリース。

#include <thread>
#include <iostream>
#include <string>
#include <mutex>
//Linux -lpthread
using namespace std;
static mutex mux;
 
void ThreadMainMux(int i)
{
    for (;;)
    {
        mux.lock();
        cout << i << "[in]" << endl;
        //std::this_thread::sleep_for和sleep,没啥太大区别,都是表示当前线程休眠一段时间,
        //休眠期间不与其他线程竞争CPU,根据函数参数,等待相应时间时间。
        //只是一个是C的函数一个是c++的函数分别对应头文件 <unistd.h> 和 < thread >
        this_thread::sleep_for(100ms);
        mux.unlock();
        this_thread::sleep_for(1ms);
    }
}
int main(int argc, char* argv[])
{
    for (int i = 0; i < 3; i++)
    {
        thread th(ThreadMainMux, i + 1);
        th.detach();
    }
 
    getchar();
    return 0;
}


2.4 タイムアウト ロック timed_mutex (長期間のデッドロックを回避するため) と再帰的ロック recursive_mutex

        Mutex にはデフォルトではタイムアウトがなく、スレッドが占有されている場合、他のスレッドは常にブロックされます。このコード方法は簡潔ですが、後続のコードのデバッグがより困難になります。たとえば、コード内に誤ってデッドラインを書いてしまった場合などです。ロック、では、デバッグ中にこのデッドロックをどのように見つけますか? 各ロックの前にログを記録して、ロックが入ったかどうかを確認します。このようなデバッグ コストは比較的高く、見つけるのは簡単ではありません。timed が追加されている限り、タイムアウトがサポートされます。

#include <thread>
#include <iostream>
#include <string>
#include <mutex>
//Linux -lpthread
using namespace std;
timed_mutex tmux;
 
void ThreadMainTime(int i){
    for (;;){
        if (!tmux.try_lock_for(chrono::milliseconds(500))){  //等待时间超过500ms就超时了
            cout << i << " try_lock_for timeout" << endl;
            continue;
        }
        cout << i << "[in]" << endl;
        this_thread::sleep_for(2000ms);  //假设要处理的业务的持续时间
        tmux.unlock();
        this_thread::sleep_for(1ms);     //防止某一个线程一直占用这个锁资源
    }
}
 
int main(int argc, char* argv[]){
    for (int i = 0; i < 3; i++){
        thread th(ThreadMainTime, i+1);
        th.detach();
    }
    getchar();
    return 0;
}

  

        多くの企業が同じロックを使用する可能性があり、同じロックが複数回ロックされる可能性があります。通常のロック (ミューテックス ロック) の場合、2 回目のロック時に例外がスローされます。例外がキャッチされない場合は、プログラムはクラッシュし、この再帰的ロックは複数回ロックされる可能性があります。現在のスレッドのロック数が 1 つ増加しますが、ロック状態は変わりません。ロックが解除されるまで、ロックの数だけロックが解除されます。カウントがゼロに達した場合にのみ解放されるため、不要なデッドロックを回避できます。 

#include <thread>
#include <iostream>
#include <string>
#include <mutex>
//Linux -lpthread
using namespace std;
recursive_mutex rmux;

void Task1(){
    rmux.lock();
    cout << "task1 [in]" << endl;
    rmux.unlock();
}
void Task2(){
    rmux.lock();
    cout << "task2 [in]" << endl;
    rmux.unlock();
}
void ThreadMainRec(int i){
    for(;;){
        rmux.lock();
        Task1();
        cout << i << "[in]" << endl;
        this_thread::sleep_for(500ms);
        Task2();
        rmux.unlock();
        this_thread::sleep_for(1ms);
    }
}
int main(int argc, char* argv[]){
    for (int i = 0; i < 3; i++){
        thread th(ThreadMainRec, i + 1);
        th.detach();
    }
    getchar();
    return 0;
}


2.5 共有ロックshared_mutexは読み取りと書き込みの問題を解決します

        現在のスレッドは、データの書き込み時に相互排他的である必要があります。つまり、他のスレッドは書き込みも読み取りもできません。現在のスレッドが読み取りを行っている間、他のスレッドは読み取りのみ可能ですが、書き込みはできません。
        これには、読み取りロックと書き込みロックの 2 つのロックが必要です。
このスレッドが読み取り専用の場合は、読み取り専用ロックを使用できます。ただし、このスレッドに変更が含まれる場合は、最初に読み取りロックを取得し、次に読み取りロックを取得する必要があります。書き込みロックを取得し、それを変更し、変更後に解放するこのメソッドを使用して共有を作成します。
        共有ロックには 2 つのロックが含まれており、1 つは共有ロック、もう 1 つはミューテックス ロックです。誰もミューテックス ロックをロックしない限り、共有ロックはすぐに返されます。誰かがミューテックス ロックをロックしている限り、共有ロックは返されません。その他ミューテックスも入力できません。
c++14 共有タイミングミューテックスshared_timed_mutex (デフォルトの一般値は C++14 でサポートされています)
c++17 共有ミューテックスshared_mutex
ミューテックスが書き込み時にのみ必要で読み取り時に必要ない場合、通常のロックを使用する方法は
?次のコードでは、読み取りのために 1 つのスレッドのみが入力できるため、多くのビジネス シナリオでは、CPU リソースが十分に活用されていません。

        原理としては、1 つのスレッドが書き込み中の場合、他のすべてのスレッドは読み取りも書き込みもできないということです。読み取りスレッドが 1 つある場合、他のスレッドは読み取りはできますが、書き込みはできません。書き込みを行う前に、すべての読み取りスレッドが読み取りを完了するまで待つ必要があるため、これはリソースが複数の人によって書き込まれ、エラーが発生しないようにします。
        これが使用する共有ロックです。読み取りロックを解除する必要があり、読み取りロックがロックされている場合、ミューテックス ロックに入ることができません。ミューテックスに入ると、他のすべての読み取りスレッドが待機状態になります。他のスレッドの書き込みロックも待機しており、同時に書き込みできるスレッドは 1 つだけです。

#include <thread>
#include <iostream>
#include <string>
#include <mutex>
#include <shared_mutex>
//Linux make: g++ -std=c++14 -o main main8.c -lpthread
using namespace std;

//shared_mutex smux;		//c++17  共享锁
shared_timed_mutex stmux;	//c++14  共享锁 
void ThreadRead(int i){
    for(;;){
        //stmux.lock_shared();共享锁,只要对方没有把互斥锁锁住,共享锁大家都可以进去。
        stmux.lock_shared();
        cout << i << " Read" << endl;
        this_thread::sleep_for(500ms);
        stmux.unlock_shared();
        this_thread::sleep_for(1ms);
    }
}
void ThreadWrite(int i){
    for(;;){
        stmux.lock_shared();
        //读取数据
        stmux.unlock_shared();
        stmux.lock(); //互斥锁 写入
        cout << i << " Write" << endl;
        this_thread::sleep_for(300ms);
        stmux.unlock();
        this_thread::sleep_for(1ms);
    }
}
int main(int argc, char* argv[]){
    for(int i = 0; i < 3; i++){
        thread th(ThreadWrite, i + 1);
        th.detach();
    }
    for(int i = 0; i < 3; i++){
        thread th(ThreadRead, i + 1);
        th.detach();
    }
    getchar();
    return 0;
}

おすすめ

転載: blog.csdn.net/qq_34761779/article/details/129226464