C++ 同時プログラミング (4): 共有データの問題、共有データを保護するためのミューテックスの使用、デッドロック

スレッド間でデータを共有する

参考ブログ

スレッド間でのデータの共有 - ミューテックスを使用して共有データを保護する

[c++11] マルチスレッドプログラミング (4) - デッドロック (Dead Lock)

C++マルチスレッドデッドロック

C++ デッドロックとその解決策

データ共有の問題

あなたと友達がしばらくアパートをシェアし、そのアパートにはキッチンとバスルームしかないことを想像してください。よほどの思い入れがない限り、同時にトイレを使うことは不可能です。また、友達が長時間トイレを占拠していて、たまたま自分もトイレが必要になった場合は不便です。同様に、コンビネーションオーブンをお持ちの場合、同時に調理することはできますが、1 人がソーセージを焼いている間にもう 1 人がケーキを焼いている場合、結果はあまり良くありません。また、シェアオフィスのトラブルも承知しています。物が完成していないのに、仕事に必要なものを誰かが借りたり、半完成品を他人が勝手に変更したりするなどです。

スレッドについても同様です。データがスレッド間で共有される場合は、どのスレッドがどのデータにどのような方法でアクセスするか、また、データが変更された場合、他のスレッドが関係する場合には、いつ、どのような通信方法で通知されるべきかという仕様に従う必要があります。同じプロセス内の複数のスレッド間でデータを簡単に共有できますが、これは絶対的な利点ではなく、場合によっては大きな欠点になる場合もあります。共有データの不適切な使用は同時実行関連のバグの大きな原因であり、「ソーセージ ケーキ」よりもはるかに悪い

悪質な条件付き競争

悪質な条件付き競合を引き起こす典型的なシナリオは、操作を完了するために、上の例の 2 つのリンク ポインターなど、2 つ以上の異なるデータを変更する必要があるというものです。この操作には 1 つの命令でのみ変更できる 2 つの個別のデータが含まれるため、当其中一份数据完成改动时,别的线程有可能不期而访. 条件付き競合は、条件が満たされる時間枠が短いため、多くの場合、検出および再現が困難です。ミューテーション操作が連続した中断のない CPU 命令ストリームで実行される場合、他のスレッドが同時にデータにアクセスしている場合でも、1 回の実行で問題が発生する可能性はほとんどありません。問題は、命令が特定の順序で実行された場合にのみ発生します。システム負荷が増加し、実行される操作の数が増加すると、このシーケンスが発生する可能性が高くなります。「家の雨漏りは、夜中に突然雨が降る」ということはほぼ避けられず、これらの問題は最も不適切な状況下で発生します。悪質な競合状態は、一般に、発生したときは「厄介」であり、アプリケーションをデバッグ環境で実行すると完全に消えることがよくあります。これは、デバッグ ツールがプログラムの内部実行タイミングにたとえわずかであっても影響を与えるためです。

ミューテックスを使用して共有データを保護する

開発者は、共有データにアクセスする前に、ミューテックスを使用して関連データをロックし、アクセスが完了した後にデータのロックを解除できます。したがって、スレッド ライブラリは、スレッドが特定のミューテックスを使用して共有データをロックする場合、データのロックが解除された後にのみ他のスレッドがそのデータにアクセスできるようにする必要があります。

lock()、unlock() ロックとロック解除

C++ では、ミューテックスは std::mutex をインスタンス化することによって作成され、メンバー関数 lock() を呼び出すことによってロックされ、unlock() によってロック解除されます。

実際には、ロックはペアで使用する必要があり、関数内でロックを使用した後は、関数の終了時にロック解除を呼び出す必要があります。

mutex my_mutex;
int a = 1;
bool func()
{
    
       
    my_mutex.lock();
    if(!a)
    {
    
    
        cout << "a = " << a << endl;
        my_mutex.unlock();
        return false;
    }
    my_mutex.unlock();
    return true;    
}

上記のコードは戻る前にロック解除を呼び出していることに注意してください。

RAII std::lock_guard

C++ 標準ライブラリは、ミューテックス用の RAII 構文テンプレート クラス std::lock_guard を提供します。これは にあり、构造的时候提供已锁的互斥量,并在析构的时候进行解锁ロックされたミューテックスが常に正しくロック解除されるようにします。

mutex my_mutex;
int a = 1;
bool func()
{
    
       
    lock_guard<mutex> my_guard(my_mutex);
    // my_mutex.lock();
    if(!a)
    {
    
    
        cout << "a = " << a << endl;
        // my_mutex.unlock();
        return false;
    }
    // my_mutex.unlock();
    return true;    
}

lock_guard のスコープを制限することで、ロックを早期に解放できます。

mutex my_mutex;
int a = 1;
bool func()
{
    
    
    // lock_guard<mutex> my_guard(my_mutex);
    // my_mutex.lock();
    if (!a)
    {
    
    
        {
    
    
            lock_guard<mutex> my_guard(my_mutex);
            cout << "a = " << a << endl;
        }
        // my_mutex.unlock();
        return false;
    }
    // my_mutex.unlock();
    return true;
}

オブジェクト指向設計ガイドライン: ミューテックスをデータ メンバーとしてクラスに配置します。

ミューテックスと保護されるデータはクラス内のプライベート メンバーとして定義する必要があり、すべてのメンバー関数は呼び出されたときにデータをロックし、最後にデータのロックを解除する必要があることに注意してください。データは破壊されません

現実は必ずしもそれほど理想的ではありません。メンバー関数が保護されたデータへのポインターまたは参照を返す場合、データ破損の可能性があることを認識する必要があります。その理由は です使用者可以通过引用或指针直接访问数据,从而绕开互斥量的保护したがって、クラスが独自のデータ メンバーを保護するためにミューテックスを使用する場合、その開発者は、谨小慎微地设计接口ミューテックスがデータへのアクセスをロックし、バックドアを残さないようにする必要があります。

class some_data
{
    
    
  int a;
  std::string b;
public:
  void do_something();
};

class data_wrapper
{
    
    
private:
  some_data data;
  std::mutex m;
public:
  template<typename Function>
  void process_data(Function func)
  {
    
    
    std::lock_guard<std::mutex> l(m);
    func(data);    // 1 传递“保护”数据给用户函数
  }
};

some_data* unprotected;

void malicious_function(some_data& protected_data)
{
    
    
  unprotected=&protected_data;
}

data_wrapper x;
void foo()
{
    
    
  x.process_data(malicious_function);    // 2 传递一个恶意函数
  unprotected->do_something();    // 3 在无保护的情况下访问保护数据
}

process_data には何の問題もないように見えますが、ユーザー定義の func を呼び出すと、foo が保護メカニズムをバイパスして関数 malicious_function を渡し、ミューテックス ロックなしで do_something を呼び出すことができることを意味します。

C++ 標準ライブラリはこの動作に対する保護を提供していないため、次の点に留意することが重要です。切勿将受保护数据的指针或引用传递到互斥锁作用域之外

デッドロック

まず、基本的な例を使用して、デッドロックとは何かを抽象化します。

面接官: 「デッドロックが何であるかを明確に説明していただければ、オファーを送ります。」
候補者: 「オファーを送っていただければ、デッドロックが何であるかを説明します。」

デッドロックは、スレッドのペアが存在し、両方のスレッドが自分のミューテックスをロックすることから始まり、相手が保持しているミューテックスを解放することを要求するいくつかの操作を実行する必要があるシナリオです。このシナリオでは、どのスレッドも適切に動作できません。なぜなら、それらはすべてお互いがミューテックスを解放するのを待っているからです。同じ操作をロックしているミューテックスが 3 つ以上ある場合、デッドロックが発生しやすくなります。

たとえば、この時点でスレッド A があった場合、最初に a をロックし、次に b をロックします。同時に別のスレッド B があり、最初に b をロックし、次に a をロックするという順序でロックを取得します。 。以下に示すように:

ここに画像の説明を挿入

図に示すように、この状況では、スレッド A はロック b を待機していますが、ロック b はロックされているため、現時点では続行できません。ロック b が解放されるまで待つ必要があり、スレッド B が最初にロック b をロックしています。 、ロック b を待機しています a を解放すると、スレッド A はスレッド B を待機し、スレッド B はスレッド A を待機することになり、デッドロックが発生します。

#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
using namespace std;

class LogFile {
    
    
    std::mutex _mu;
    std::mutex _mu2;
    ofstream f;
public:
    LogFile() {
    
    
        f.open("log.txt");
    }
    ~LogFile() {
    
    
        f.close();
    }
    void shared_print(string msg, int id) {
    
    
        std::lock_guard<std::mutex> guard(_mu);
        std::lock_guard<std::mutex> guard2(_mu2);
        f << msg << id << endl;
        cout << msg << id << endl;
    }
    void shared_print2(string msg, int id) {
    
    
        std::lock_guard<std::mutex> guard(_mu2);
        std::lock_guard<std::mutex> guard2(_mu);
        f << msg << id << endl;
        cout << msg << id << endl;
    }
};

void function_1(LogFile& log) {
    
    
    for(int i=0; i>-100; i--)
        log.shared_print2(string("From t1: "), i);
}

int main()
{
    
    
    LogFile log;
    std::thread t1(function_1, std::ref(log));

    for(int i=0; i<100; i++)
        log.shared_print(string("From main: "), i);

    t1.join();
    return 0;
}

実行後、プログラムがスタックしてデッドロックになっていることがわかります。プログラムの実行中に、次のような現象が発生する場合があります。

Thread A              Thread B
_mu.lock()          _mu2.lock()
   //死锁               //死锁
_mu2.lock()         _mu.lock()

よくあるデッドロック状況

1. ロックの解除を忘れた

mutex _mutex;
void func()
{
    
    
	_mutex.lock();
	if (xxx)
	  return;
	_mutex.unlock();
}

2. シングルスレッドによるロックの繰り返し適用

mutex _mutex;
void func()
{
    
    
	_mutex.lock();
	 //do something....
	_mutex.unlock();
}
 
void data_process() {
    
    
	_mutex.lock();
	func();
	_mutex.unlock();
}

3. デュアルスレッドマルチロックアプリケーション

mutex _mutex1;
mutex _mutex2;
 
void process1() {
    
    
	_mutex1.lock();
	_mutex2.lock();
	//do something1...
	_mutex2.unlock();
	_mutex1.unlock();
}
 
void process2() {
    
    
	_mutex2.lock();
	_mutex1.lock();
	//do something2...
	_mutex1.unlock();
	_mutex2.unlock();
}

4.リングロックの適用

/*
*             A   -  B
*             |      |
*             C   -  D
*/

デッドロックの解決策

1.mutex比較可能なアドレスは、毎回アドレスの小さい方からロックする

複数のロックを取得する必要があり、std::lock を使用できないという厳しい条件がある場合、ガイドラインとしては、各スレッドが同じ順序でロックを取得することが保証されることになります。

if(&_mu < &_mu2){
    
    
    _mu.lock();
    _mu2.unlock();
}
else {
    
    
    _mu2.lock();
    _mu.lock();
}

2. 同時に 1 つのミューテックスのみをロックしてみます

{
    
    
 std::lock_guard<std::mutex> guard(_mu2);
 //do something
    f << msg << id << endl;
}
{
    
    
 std::lock_guard<std::mutex> guard2(_mu);
 cout << msg << id << endl;
}

3. ミューテックスで保護された領域ではユーザー定義コードを使用しないでください。ユーザーのコードが他のミューテックスを操作する可能性があるためです。

{
    
    
 std::lock_guard<std::mutex> guard(_mu2);
 user_function(); // never do this!!!
    f << msg << id << endl;
}

4. 複数のミューテックスを同時にロックしたい場合は、次を使用します。std::lock()

std::lock(_mu, _mu2);

5. 階層ロックを使用する

階層ロックを使用し、ミューテックスをラップし、ロックの階層化された属性を定義し、毎回ロックを高位から低位の順に実行します。

コードがロック操作を実行しようとすると、現在下位レベルからのロックを保持しているかどうかを確認し、保持している場合は現在のミューテックスのロックを禁止します。

hierarchical_mutex high_level_mutex(10000);
hierarchical_mutex low_level_mutex(5000);

int do_low_level_stuff();

int low_level_func() {
    
    
  std::lock_guard<hierarchical_mutex> lk(low_level_mutex);
  return do_low_level_stuff(); 
}

void high_level_stuff(int some_param);

void high_level_func() {
    
    
  std::lock_guard<hierarchical_mutex> lk(high_level_mutex);
  high_level_stuff(low_level_func());
}

void thread_a() {
    
    
  high_level_func(); 
}

hierarchical_mutex other_mutex(100); 
void do_other_stuff();

void other_stuff() {
    
    
  high_level_func();
  do_other_stuff(); 
}

void thread_b() {
    
    
  std::lock_guard<hierarchical_mutex> lk(other_mutex);
  other_stuff(); 
}

thread_a は階層ルールに従いますが、thread_b は従いません。thread_a が high_level_func を呼び出しているため、高レベルのミューテックス high_level_mutex がロックされ、次に low_level_func を呼び出そうとしていることがわかります。この時点では、低レベルのミューテックス low_level_mutex がロックされており、これは上記のルールと一致しています。ハイレベルをロックしてからローレベルをロックする

thread_b の動作はそれほど楽観的ではなく、最初にレベル 100 の other_mutex をロックし、次に高レベルの high_level_mutex をロックしようとします。このとき、エラーが発生したり、例外がスローされたり、プログラムが停止したりする可能性があります。直接終了される

おすすめ

転載: blog.csdn.net/Solititude/article/details/131738758