データ共有のデッドロック
7391 ワード
反発量を使用するとデッドロックになる可能性があります.デッドロックになるには、次の4つの条件を満たす必要があります.
1、反発使用(リソース排他) 1つのリソースは1つのプロセスでのみ使用できます. 2、占拠してはいけない(剥奪してはいけない) 資源申請者は強制的に資源占有者から資源を奪うことができず、資源は占有者が自発的に釈放するしかない. 3、要求と保持(部分分配、占有申請) 1つのプロセスは、新しいリソースを申請すると同時に、既存のリソースの占有を維持します(これこそ動的申請、動的割り当てです). 4、ループ待ち プロセス待機キューが存在します {P1 , P2 , … , Pn}, ここで、P 1はP 2が占有する資源を待ち、P 2はP 3が占有する資源を待ち、…、PnはP 1が占有する資源を待ち、プロセス待ちループを形成する
反発量を使用する場合、デッドロックが2つ発生しやすい場合があります.
1、2つ(または複数のスレッド)はそれぞれ1つの反発量を有し、さらに1つ(または複数の)反発量が必要であり、この反発量は相手によってロックされている.
1つの最も簡単な状況:スレッドAとスレッドBは、両方とも2つのロックが必要ですが、各スレッドは1つを持っています.
ロックの順序が一致しない場合もあります.例えば、2つのロックm 1とm 2を追加すると、1つの関数がm 1にロックされてからm 2にロックされ、もう1つの関数が逆にデッドロックが発生しやすい.
例:https://github.com/KangRoger/CppConcurrencyInAction/blob/master/DeadLocktest1.cpp
2、同一の反発量に対して二つのロック(反発量は非再帰)
例:https://github.com/KangRoger/CppConcurrencyInAction/blob/master/DeadLocktest2.cpp
2つ以上の反発量を使用する場合、ロック順序が同じであればデッドロックが発生しないことを確認します.
ロックの順序を決めると、デッドロックになることもあります.たとえば、2つのロックは1つのクラスの2つのインスタンスを保護し、1つの関数はこの2つのインスタンス(関数の最初のパラメータが先にロックされ、2番目のパラメータが後にロックされる)を交換し、交換された2つのインスタンスが同じインスタンスである場合もデッドロックが発生します.
C++標準ライブラリでは、std::lock関数を使用して、2つ以上の反発量を一度にロックし、デッドロックが発生しないことを確認する方法があります.
std::lockが複数の反発量にロックをかけると、そのうちの1つがロックに失敗すると異常が放出され、ロックされた反発量も自動的にロックが解放される.
「Linuxマルチスレッドサーバプログラミング」では、複数の反発量にロックをかける場合、反発量アドレスの高低に従ってロックをかける方法を見ました.これにより、ロック順序が確保されます.
デッドロックを回避する方法
デッドロックは、ロックを使用するときに発生するだけでなく(これは最も一般的ですが)、2つのスレッドを作成し、各スレッドで相手のjoin関数を呼び出すと、デッドロックになります.両方のスレッドは、相手が先に終わるのを待っています.デッドロックを避けるには、このスレッドがあなたを待つ可能性がある場合は、このスレッドを待たないでください.
1、ロックのネストに鍵をかけない
鍵を1つ持っているときは、この鍵に鍵をかけないでください.複数のロックを使用する場合はstd::lockを使用します.
2、ロックを持っている場合、他人から提供された関数を呼び出さない
他の人のコードがどのように実現されているのか分からないので、ロックを使用しているかどうか分かりません.
3、複数のロックを施錠する場合は、順番を固定します.
複数のロックがかかっていてstd::lockが使用できない場合は、各スレッドで同じ順序でロックするのが最善です.
4、階層化してロックを使う
プログラムをいくつかの階層に分ける.各階層で使用されるロックを区別します.1つのスレッドがより低いレベルのロックを持っている場合、高レベルのロックは使用できません.プログラムの実行時に異なるロックに階層番号を付けて、各スレッドが持つロックを記録できます.
std::unique_を使用するロックフレキシブルロック
std::unique_lock比std::lock_guardはより柔軟で、まず反発量の所有権を占めてから鍵をかけることができます.lock_guardは制御権を占有しながら鍵をかけた.
トランスファロックの制御権
std::unique_lockのインスタンスはロックの制御権を占めておらず、インスタンスを移動することでロックの制御権をインスタンス間で伝達することができる.場合によっては、std::move()などの移動は原子です.std::unique_lockは移行できますが、コピーはできません.
1つの用法は、1つの関数で1つの反発量にロックをかけ、ロックの制御権を別の関数に渡すことである.
適切な粒度でロックする
ロックが必要な場所でロックを使用し、ロックの有効領域をできるだけ小さくします.I/O操作に関わる関数では、できるだけロックを使用しないでください.I/O操作に時間がかかるからです.std::unique_lock()は,共有データを使用しない場合にロックを解除し,必要に応じてロックすることができる.
ロック保護されたデータを使用する場合、データ量が非常に小さく、ロック時間よりもコピー時間が小さい場合は、コピーを考慮することができます.たとえば、2つのintタイプのデータを比較すると、ロックで保護するよりもコピー時間が短くなります.
1、反発使用(リソース排他) 1つのリソースは1つのプロセスでのみ使用できます. 2、占拠してはいけない(剥奪してはいけない) 資源申請者は強制的に資源占有者から資源を奪うことができず、資源は占有者が自発的に釈放するしかない. 3、要求と保持(部分分配、占有申請) 1つのプロセスは、新しいリソースを申請すると同時に、既存のリソースの占有を維持します(これこそ動的申請、動的割り当てです). 4、ループ待ち プロセス待機キューが存在します {P1 , P2 , … , Pn}, ここで、P 1はP 2が占有する資源を待ち、P 2はP 3が占有する資源を待ち、…、PnはP 1が占有する資源を待ち、プロセス待ちループを形成する
反発量を使用する場合、デッドロックが2つ発生しやすい場合があります.
1、2つ(または複数のスレッド)はそれぞれ1つの反発量を有し、さらに1つ(または複数の)反発量が必要であり、この反発量は相手によってロックされている.
1つの最も簡単な状況:スレッドAとスレッドBは、両方とも2つのロックが必要ですが、各スレッドは1つを持っています.
ロックの順序が一致しない場合もあります.例えば、2つのロックm 1とm 2を追加すると、1つの関数がm 1にロックされてからm 2にロックされ、もう1つの関数が逆にデッドロックが発生しやすい.
例:https://github.com/KangRoger/CppConcurrencyInAction/blob/master/DeadLocktest1.cpp
#include<thread>
#include<mutex>
#include<unistd.h>
class Test
{
private:
std::mutex m1;
std::mutex m2;
public:
void fun1()
{
std::lock_guard<std::mutex> guard1(m1);
// ,
sleep(1);
std::lock_guard<std::mutex> guard2(m2);
}
void fun2()
{
std::lock_guard<std::mutex> guard1(m2);
// ,
sleep(1);
std::lock_guard<std::mutex> guard2(m1);
}
};
void fun1(Test *p)
{
p->fun1();
}
void fun2(Test *p)
{
p->fun2();
}
int main()
{
Test t;
std::thread A(fun1, &t);
std::thread B(fun2, &t);
A.join();
B.join();
return 0;
}
2、同一の反発量に対して二つのロック(反発量は非再帰)
例:https://github.com/KangRoger/CppConcurrencyInAction/blob/master/DeadLocktest2.cpp
#include<thread>
#include<mutex>
#include<iostream>
class Test
{
private:
std::mutex m1;
public:
void fun1()
{
std::lock_guard<std::mutex> guard1(m1);
fun2();
}
void fun2()
{
std::lock_guard<std::mutex> guard1(m1);
}
};
void fun(Test *p)
{
p->fun1();
std::cout << "fun1" << std::endl;
}
int main()
{
Test t;
std::thread A(fun, &t);
A.join();
return 0;
}
2つ以上の反発量を使用する場合、ロック順序が同じであればデッドロックが発生しないことを確認します.
ロックの順序を決めると、デッドロックになることもあります.たとえば、2つのロックは1つのクラスの2つのインスタンスを保護し、1つの関数はこの2つのインスタンス(関数の最初のパラメータが先にロックされ、2番目のパラメータが後にロックされる)を交換し、交換された2つのインスタンスが同じインスタンスである場合もデッドロックが発生します.
C++標準ライブラリでは、std::lock関数を使用して、2つ以上の反発量を一度にロックし、デッドロックが発生しないことを確認する方法があります.
#include<mutex>
class some_big_object
{
};
void swap(some_big_object& lhs, some_big_object& ths);
class X
{
private:
some_big_object some_detail;
std::mutex m;
public:
X(some_big_object const& sd) :some_detail(sd){}
friend void swap(X& lhs, X& rhs)
{
if (&lhs == rhs)
return;
std::lock(lhs.m, rhs.m);//
// ,
// ,lock_guard
std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
swap(lhs.some_detail, rhs.some_detail);
}
};
std::lockが複数の反発量にロックをかけると、そのうちの1つがロックに失敗すると異常が放出され、ロックされた反発量も自動的にロックが解放される.
「Linuxマルチスレッドサーバプログラミング」では、複数の反発量にロックをかける場合、反発量アドレスの高低に従ってロックをかける方法を見ました.これにより、ロック順序が確保されます.
デッドロックを回避する方法
デッドロックは、ロックを使用するときに発生するだけでなく(これは最も一般的ですが)、2つのスレッドを作成し、各スレッドで相手のjoin関数を呼び出すと、デッドロックになります.両方のスレッドは、相手が先に終わるのを待っています.デッドロックを避けるには、このスレッドがあなたを待つ可能性がある場合は、このスレッドを待たないでください.
1、ロックのネストに鍵をかけない
鍵を1つ持っているときは、この鍵に鍵をかけないでください.複数のロックを使用する場合はstd::lockを使用します.
2、ロックを持っている場合、他人から提供された関数を呼び出さない
他の人のコードがどのように実現されているのか分からないので、ロックを使用しているかどうか分かりません.
3、複数のロックを施錠する場合は、順番を固定します.
複数のロックがかかっていてstd::lockが使用できない場合は、各スレッドで同じ順序でロックするのが最善です.
4、階層化してロックを使う
プログラムをいくつかの階層に分ける.各階層で使用されるロックを区別します.1つのスレッドがより低いレベルのロックを持っている場合、高レベルのロックは使用できません.プログラムの実行時に異なるロックに階層番号を付けて、各スレッドが持つロックを記録できます.
#include <stdexcept>
#include<thread>
#include<mutex>
class hierarchical_mutex// mutex lock,unlock,trylock
{
std::mutex internal_mutex;
unsigned long const hierarchy_value;// mutex
unsigned long previous_hierarchy_value;// mutex ,
//thread_local (instance)
static thread_local unsigned long this_thread_hierarchy_value;// , thread_local
void check_for_hierarchy_violation()
{ // mutex ,
if (this_thread_hierarchy_value <= hierarchy_value)
{
throw std::logic_error("mutex hierarchy violated");
}
}
void update_hierarchy_value()
{
previous_hierarchy_value = this_thread_hierarchy_value;
this_thread_hierarchy_value = hierarchy_value;
}
public:
explicit hierarchical_mutex(unsigned long value) :
hierarchical_mutex(value), previous_hierarchy_value(0){}
void lock()
{
// 、 、
check_for_hierarchy_violation();
internal_mutex.lock();
update_hierarchy_value();
}
void unlock()
{
// 、
this_thread_hierarchy_value = previous_hierarchy_value;
internal_mutex.unlock();
}
bool try_lock()
{
check_for_hierarchy_violation();
if (!internal_mutex.try_lock())
return false;
update_hierarchy_value();
return true;
}
};
thread_local unsigned long hierarchical_mutex::this_thread_hierarchy_value(ULLONG_MAX);
std::unique_を使用するロックフレキシブルロック
std::unique_lock比std::lock_guardはより柔軟で、まず反発量の所有権を占めてから鍵をかけることができます.lock_guardは制御権を占有しながら鍵をかけた.
class X
{
private:
some_big_object some_detail;
std::mutex m;
public:
X(some_big_object const& sd) :some_detail(sd){}
friend void swap(X& lhs, X& rhs)
{
if (&rhs == &lhs)
return;
// , std::defer_lock
std::unique_lock<std::mutex> lock_a(lhs.m, std::defer_lock);
std::unique_lock<std::mutex> lock_a(rhs.m, std::defer_lock);
std::lock(lock_a, lock_b);
swap(lhs.some_detail, rhs.some_detail);
}
};
トランスファロックの制御権
std::unique_lockのインスタンスはロックの制御権を占めておらず、インスタンスを移動することでロックの制御権をインスタンス間で伝達することができる.場合によっては、std::move()などの移動は原子です.std::unique_lockは移行できますが、コピーはできません.
1つの用法は、1つの関数で1つの反発量にロックをかけ、ロックの制御権を別の関数に渡すことである.
std::unique_lock<std::mutex> get_lock()// ,
{
extern std::mutex some_mutex;//
std::unique_lock<std::mutex> lk(some_mutex);
prepare_data();
return lk;
}
void process_data()
{
std::unique_lock<std::mutex> lk(get_lock());
do_something();
}
適切な粒度でロックする
ロックが必要な場所でロックを使用し、ロックの有効領域をできるだけ小さくします.I/O操作に関わる関数では、できるだけロックを使用しないでください.I/O操作に時間がかかるからです.std::unique_lock()は,共有データを使用しない場合にロックを解除し,必要に応じてロックすることができる.
void get_and_process_data()
{
std::unique_lock<std::mutex> my_lock(the_mutex);//
some_class data_to_process=get_next_data_chunk();
my_lock.unlock();//
result_type result=process(data_to_process);
my_lock.lock();
write_result(data_to_process,result);
}
ロック保護されたデータを使用する場合、データ量が非常に小さく、ロック時間よりもコピー時間が小さい場合は、コピーを考慮することができます.たとえば、2つのintタイプのデータを比較すると、ロックで保護するよりもコピー時間が短くなります.
class Y
{
private:
int some_detail;
mutable std::mutex m;
int get_detail() const
{
std::lock_guard<std::mutex> lock_a(m);
return some_detail;
}
public:
Y(int sd):some_detail(sd){}
friend bool operator==(Y const& lhs, Y const& rhs)
{
if(&lhs==&rhs)
return true;
int const lhs_value=lhs.get_detail();
int const rhs_value=rhs.get_detail();
return lhs_value==rhs_value;
}
};