複数のスレッド間のC ++データの相互作用

ブログhttps://blog.csdn.net/hai008007/article/details/80246437を参照して、整理および変更してください。

同じプロセス内の複数のスレッド間のデータの相互作用は避けられません。キューと共有データは、複数のスレッド間のデータの相互作用を実現するための一般的な方法です。カプセル化されたキューは、比較的エラーが発生しにくく、共有データが最も基本的でエラーが発生しやすいです。次の例に示すように、データの競合が発生するため、つまり、メモリブロックの読み取りと書き込みなど、複数のスレッドが同時にリソースを取得しようとするためです。

#include <iostream>
#include <thread>
 
using namespace std;
 
#define COUNT 10000
 
void inc(int *p){
    
    
    for(int i = 0; i < COUNT; i++){
    
    
        (*p)++;
    }
}
 
int main()
{
    
    
    int a = 0;
    
    thread ta(inc, &a);
    thread tb(inc, &a);
    
    ta.join();
    tb.join();
    
    cout << " a = " << a << endl;
    return 0;
}

上記の例は単純なデータ交換です。2つのスレッドが&aのメモリアドレスを同時に読み書きしていることがわかります。表面的には、2つのスレッドが実行された後、最終的な値はCOUNT * 2になるはずですが、実際にはそうではありません。これは、操作のためにこのメモリに同時にアクセスする必要がある2つのスレッドが存在する可能性があるためです。 、スレッドが中断されます。現象。この問題を解決するために、文字、整数、ポインタなどの単純な基本タイプのデータの場合、C ++はアトミックテンプレートクラスatomicを提供し、複雑なオブジェクトの場合、相互排他クラスmutexなどの最も一般的に使用されるロックメカニズムを提供します。 、ドアロックlock_guard、一意のロックunique_lock、条件変数condition_variableなど。

std :: atomic

スレッドの場合、アトミックタイプは「リソースタイプ」データに属します。つまり、通常、複数のスレッドは単一のアトミックタイプのコピーにしかアクセスできません。
したがって、C ++ 11では、アトミックタイプはテンプレートパラメータタイプからのみ構築できます。
この標準では、アトミックタイプがコピー構築、移動構築、operator =の使用などの操作を実行することは許可されていません。実際、アトミッククラステンプレートのコピー構築、移動構築、operator =などの操作はデフォルトで削除されます。
ただし、アトミック型の変数からテンプレートパラメータ型Tの変数を作成することは可能です。これは、アトミッククラステンプレートが常にatomic <T>からTへの型変換関数を定義するためです。必要に応じて、コンパイラは暗黙的に型変換を行います。アトミックタイプから対応するテンプレートパラメータタイプへの変換を完了します。
C11ヘッダーファイル<cstdatomic>は、組み込み型に対応するアトミック型を定義するだけです。

アトミック の種類
atomic_bool ブール
atomic_char char
atomic_schar 符号付き文字
atomic_uchar unsigned char
atomic_int int
atomic_uint unsigned int
atomic_short ショート
atomic_ushort unsigned short
atomic_long 長いです
atomic_ulong unsigned long
atomic_llong 長い長い
atomic_ullong unsigned long long
atomic_char16_t char16_t
atomic_char32_t char32_t
atomic_wchar_t wchar_t

繰り返しますが、例えば:

#include <iostream>
#include <thread>
#include <atomic>
 
using namespace std;
 
#define COUNT 10000
 
void inc(atomic<int> *p){
    
    
    for(int i = 0; i < COUNT; i++){
    
    
        (*p)++;
    }
}
 
int main()
{
    
    
    atomic<int> a{
    
    0};
    
    thread ta(inc, &a);
    thread tb(inc, &a);
    
    ta.join();
    tb.join();
    
    cout << " a = " << a << endl;
    return 0;
}

std :: lock_guard

最初に小さな例を見てみましょう。

mutex m;
m.lock();
sharedVariable= getVar();
m.unlock();

このコードでは、ミューテックスmは、キー部分sharedVariable = getVar();へのアクセスがシーケンシャルであることを保証します。
順次手段:この特殊なケースでは、各スレッドがキー部分に順番にアクセスします。
コードは単純ですが、デッドロックが発生しがちです。重要な部分が例外をスローした場合、またはプログラマーが単にミューテックスのロックを解除するのを忘れた場合、デッドロックが発生します。

std :: lock_guardを使用すると、よりエレガントに行うことができます。

{
    
    
  std::mutex m,
  std::lock_guard<std::mutex> lockGuard(m);
  sharedVariable= getVar();
}

それは簡単です。しかし、開始ブラケット{および終了ブラケット}とは何ですか?
std :: lock_guardのライフサイクルがこの{}でのみ有効であることを保証するため。
つまり、ライフサイクルがクリティカルゾーンを離れると、そのライフサイクルは終了します。
正確には、その時点で、std :: lock_guardのデストラクタが呼び出され、はい、ミューテックスが解放されました。プロセスは完全に自動化されています。さらに、sharedVariable = getVar()がミューテックスも解放するときにgetVar()が例外をスローすると、もちろん、関数本体またはループのスコープもオブジェクトのライフサイクルを制限します。

#include <iostream>
#include <thread>
#include <mutex>
 
using namespace std;
 
#define COUNT 10000

static mutex g_mutex;
 
void inc(int *p){
    
    
    for(int i = 0; i < COUNT; i++){
    
    
    	lock_guard<mutex> lck(g_mutex);
        (*p)++;
    }
}
 
int main()
{
    
    
    int a{
    
    0};
    
    thread ta(inc, &a);
    thread tb(inc, &a);
    
    ta.join();
    tb.join();
    
    cout << " a = " << a << endl;
    return 0;
}

さらに、unique_lock()も使用できます。
Unique_lockはクラステンプレートです。通常、lock_guard(推奨); lock_guardはmutexのlock()とunlock()を置き換えます。unique_lockはlock_guardよりもはるかに柔軟性があり、効率が低く、少し多くのメモリを消費します。特定のunique_lock()の使用法は個別に説明されているため、ここでは繰り返しません。

std :: mutex

std :: mutexは、C ++ 11の最も基本的なミューテックスです。std:: mutexオブジェクトは、排他的所有権の特性を提供します。つまり、std :: mutexオブジェクトの再帰的ロックをサポートしませんが、std :: recursive_lockはミューテックスです。オブジェクトは再帰的にロックできます。

#include <iostream>
#include <thread>
#include <mutex>
 
using namespace std;
 
#define COUNT 10000

static mutex g_mutex;
 
void inc(int *p){
    
    
    for(int i = 0; i < COUNT; i++){
    
    
    	g_mutex.lock();
        (*p)++;
        g_mutex.unlock();
    }
}
 
int main()
{
    
    
    int a{
    
    0};
    
    thread ta(inc, &a);
    thread tb(inc, &a);
    
    ta.join();
    tb.join();
    
    cout << " a = " << a << endl;
    return 0;
}

std :: condition_variable

スレッド間のイベント通知のために、C ++ 11は条件変数クラスcondition_variable(pthread_cond_tのカプセル化と見なすことができます)を提供します。条件変数を使用すると、1つのスレッドが他のスレッドからの通知を待つことができます(wait、wait_for、wait_until) 、または他のスレッドに通知(notify_one、notify_all)を送信します。条件変数はロックと組み合わせて使用​​する必要があります。待機中は、ロック解除と再ロックが行われるため、待機中に手動でロック解除およびロックできるロックを使用する必要があります。 、unique_lockなどですが、lock_guardは使用できません。例は、次のとおりです。

#include <thread>
#include <iostream>
#include <condition_variable>

# define THREAD_COUNT 10

using namespace std;
mutex m;
condition_variable cv;

int main(void){
    
    
    thread** t = new thread*[THREAD_COUNT];
    int i;
    for(i = 0; i < THREAD_COUNT; i++){
    
    
	    t[i] = new thread( [](int index){
    
    
	        unique_lock<mutex> lck(m);
	        cv.wait_for(lck, chrono::hours(1000));
	        cout << index << endl;}, i );
            
 	    this_thread::sleep_for( chrono::milliseconds(50) );
    }
    
    for(i = 0; i < THREAD_COUNT; i++){
    
    
	    lock_guard<mutex> _(m);
 	    cv.notify_one();
    }
    
    for(i = 0; i < THREAD_COUNT; i++){
    
    
 	    t[i]->join();
	    delete t[i];
    }
    delete t;
    
    return 0;
}

プログラムをコンパイルして実行し、結果を出力した後、条件変数の順序が保証されていないことがわかります。つまり、最初に待機を呼び出すものが最初に起動されない場合があります。

std :: promise / future

Promise / futureを使用すると、ロックの問題を考慮せずにスレッド間の単純なデータ相互作用を実行できます。スレッドAはデータをpromise変数に保存し、別のスレッドBはこのpromise変数のget_future()を介してデータを取得できます。スレッドAはまだpromise変数に値を割り当てていません。スレッドBは、promise変数の割り当てを待つこともできます。

#include <thread>
#include <iostream>
#include <future>

using namespace std;

promise<string> val;

int main(void){
    
    
    thread ta([](){
    
    
	    future<string> fu = val.get_future();
	    cout << "waiting promise->future" << endl;
	    cout << fu.get() << endl;
    });
    
    thread tb([](){
    
    
	    this_thread::sleep_for( chrono::milliseconds(5000) );
	    val.set_value("promise is set");
    });
    
    ta.join();
    tb.join();
    
    return 0;
}

future変数はget()を1回しか呼び出すことができません。get()を複数回呼び出す必要がある場合は、shared_futureを使用できます。promise/ futureを介してスレッド間で例外を渡すこともできます。

std :: packaged_task

呼び出し可能オブジェクトをpromiseと組み合わせると、packaged_taskになり、操作をさらに簡素化できます。

#include <thread>
#include <iostream>
#include <mutex>
#include <future>

using namespace std;

static mutex g_mutex;

int main(void){
    
    
    auto run = [=](int index){
    
     
		{
    
    
	    	lock_guard<mutex> lck(g_mutex);
	    	cout << "tasklet " << index << endl;
		}
		this_thread::sleep_for( chrono::seconds(5) );
		return index * 1000;
    };
    
    packaged_task<int(int)> pt1(run);
    packaged_task<int(int)> pt2(run);
    thread t1( [&](){
    
    pt1(2);} );
    thread t2( [&](){
    
    pt2(3);} );

    int f1 = pt1.get_future().get();
    int f2 = pt2.get_future().get();
    cout << "task result=" << f1 << endl;
    cout << "task result=" << f2 << endl;

    t1.join();
    t2.join();
    
    return 0;
}

std :: async

packaged_taskをasync()関数であるスレッドと組み合わせることもできます。async()関数を使用して実行コードを開始し、将来のオブジェクトを返してコードの戻り値を保存します。スレッドなどを明示的に作成および破棄する必要はありませんが、C ++ 11ライブラリの実装によっていつ実行するかが決まります。スレッドの作成と破棄、および複数のスレッドの作成など。例は次のとおりです。

#include <thread>
#include <iostream>
#include <mutex>
#include <future>
#include <vector>

# define COUNT 1000000

using namespace std;

static long do_sum(vector<long> *arr, size_t start, size_t count){
    
    
    static mutex m;
    long sum = 0;
    
    for(size_t i = 0; i < count; i++){
    
    
	    sum += (*arr)[start + i];
    }
    
    {
    
    
	    lock_guard<mutex> lck(m);
	    cout << "thread " << this_thread::get_id() << ", count=" << count
	        << ", start="<< start << ", sum=" << sum << endl;
    }
    return sum;
}

int main(void){
    
    
    vector<long> data(COUNT);
    for(size_t i = 0; i < COUNT; i++){
    
    
        data[i] = random() & 0xff;
    }
    
    vector< future<long> > result;
    
    size_t ptc = thread::hardware_concurrency() * 2;
    for(size_t batch = 0; batch < ptc; batch++) {
    
    
	    size_t batch_each = COUNT / ptc;
	    if (batch == ptc - 1) {
    
    
	        batch_each = COUNT - (COUNT / ptc * batch);
	    }
	    result.push_back(async(do_sum, &data, batch * batch_each, batch_each));
    }

    long total = 0;
    for(size_t batch = 0; batch < ptc; batch++) {
    
    
	    total += result[batch].get();
    }
    cout << "total=" << total << endl;
    
    return 0;
}

要約すると、上記はいくつかの異なるマルチスレッド方式であり、使用されている例は非常に単純です。
特定のさまざまな方法の詳細については、ここにリンクがあります。フォローアップ、学習を続けます。

おすすめ

転載: blog.csdn.net/qq_24649627/article/details/112557135