『Linuxマルチスレッドサービス側プログラミング-muduo C++ネットワークライブラリを使用する』学習ノート——第二章
本カタログ第2章スレッド同期要約 反発量(mutex) 非再帰的mutex のみデッドロック 条件変数 読み書きロックと信号量 は使用しないでください.スレッドの安全な単一のモードは を実現する. shared_を使用ptrはcopy-on-write を実現する
第2章スレッド同期要約
スレッド同期の4つの原則は、重要度に基づいてソートされます.は、できるだけオブジェクトを共有することなく、共有オブジェクトは、変更不可能なオブジェクト を優先的に選択する.高度な同時プログラミングコンポーネント を使用する.低級同期原語を用いる、非再帰(再入不可)の反発量と条件変数のみを用い、読み書きロック、信号量 を用いる.原子レベル整数を除いて、ロックフリーのコードは記述されず、カーネルレベル同期原語 は使用されない.
はんぱつりょう
著者の陳碩氏が提案した反発量の使用原則: RAIIの原則に従ってmutexの作成、破棄、ロック、ロック解除 を行う非再帰(再入不可)のmutex を用いる. lock()とunlock()を手動で呼び出すことなく、すべてスタック上のGuardオブジェクトに渡し、ロック期間が臨界領域と同じ長さ になるようにする. Guardロックの順序に注意し、デッドロック を防止する.
副次的な原則:はプロセス間mutexを使用せず、プロセス間ではtcp通信 のみを使用する.ロック解除は同一スレッド(Guardは保証可能) のみである. PTHREAD_を使用可能MUTEX_ERRORCHECKによるエラー 非再帰的なmutexのみ
再帰的なmutexとは、同じスレッドでmutexを繰り返しロックできることを意味します.
次のコードによるvectorの共有を考える
mutexが再入力可能であれば、2つの関数を同時に実行できます.push_backは反復器の失効を引き起こす可能性がある.再読み込み不可の場合はdoSomeThingがpostを呼び出します.デッドロックが発生します.
1つの機能関数は、ロックされたバージョンとロックされていないバージョンに分けられ、2つのエラーが発生する可能性があります.誤ってロックバージョンを使用し、デッドロックを招いた. は、ロックされていないバージョンを誤用し、データを保護していません.
++エラー2++の場合、現在のスレッドがmutexにロックされていないことを保証するために、関数の先頭に断言を加えることができます.
++エラー1++単独ディスカッション
デッドロック
デッドロックインスタンス
上記のコードでは,InventoryのprintAll()はRequestの~Requset()より0.5 s遅れて起動し,両者とも1 s後に関数を呼び出し,自分のmutexを申請し,ループ待ちをもたらす.
じょうけんへんすう
mutexはロックにのみ使用され、条件変数はある条件の達成(ブール式が真である)を待つために使用され、学名管程(monitor).条件変数の使い方は1つだけです.
wait側の場合:はmutex保護ブール式を使用する必要があります. mutexロック後にwait()が呼び出されます. ブール判断と*wait()*はwhileに入れます.
注意:
mutexがロック解除され、現在のスレッドがブロックされます.lambdaがtrueを返すまで、ブロックが終了し、mutexが再ロックされます.while式を使用したのと同じです.unique_lockは汎用反発包装器であり、遅延ロック、ロックの期限付き試行、再帰ロック、所有権移転、および条件変数とともに使用することを可能にする.
signal/broadcastエンドの場合:理論上、signalは必ずしも鍵をかける必要はない. 一般的にsignalの前にブール式(条件が真である)を変更します. ブール式を変更するにはmutex保護が必要です. signalとbroadcastは異なり、前者は1つのスレッドを起動し、資源は直接利用可能であり、後者はすべてのスレッドを起動し、状態が変化したことを示すしかないため、whileを使用して判断条件を絶えず判断し、虚偽の起動を防止しなければならない.
条件変数の場合,下位層の同期原語は直接使用することは少なく,一般に上位層の同期措置を実現するために用いられる.カウントダウンはよく使われる同期手段で、2つの用途があります.プライマリ・スレッドは、各スレッドが一定のタスクを完了するまで、複数のサブスレッドを開始し、プライマリ・スレッドは実行を続行します. メインスレッドは複数のサブスレッドを開始し、メインスレッドが一定のタスクを完了すると、サブスレッドが実行を開始する.
読み書きロックや信号量は使わないでください
初心者はたくさん読んで、少ないシーンを書くことに出会って、mutexをrwlockに置き換えて、正確ではありません: read lockで誤って共有データを書き込み操作し、新機能の場合によく見られる. 臨界領域が小さい場合、ロック競合が大きくない場合、mutexはrwlockよりも効率的である可能性があります.rwlockはreader数を更新するためです. 一部のread lockでは、write lockに昇格できる場合、データ破損やデッドロックなどの一連の問題が導入されます. 一部のwrite lockはread lockをブロックし、読み取り遅延を増加させる.
信号量(Semaphore)は条件変数に取って代わることができ,著者らは経験がない.
スレッドセキュリティの単一モード実装
上記コードではプログラム終了時の破棄機能を実現するためにatexitを用いており,この関数の解釈は以下の通りである.
関数名:atexit機能:登録終了関数(main実行終了後に呼び出される関数)用法:int atexit(void(*func)(void);注意:atexit()に登録されている関数タイプは、パラメータを受け入れないvoid関数であり、exitがこれらの登録関数を呼び出す順序は、登録時の順序とは逆です.作者:Quinn 0918原文:https://blog.csdn.net/Quinn0918/article/details/70457370
マルチスレッドがpthreadをどのように呼び出しても、登録されたinit関数を保証できます.once,initは一度だけ呼び出されます.
このコードは、コンパイル期間において、Tタイプが不完全なタイプ(サイズ0)であるか否かを検査することを目的とする.本質はtypedefを用いてchar配列別名を定義することであり、Tタイプが完全なタイプである場合、char[1]配列別名はT_と定義されるmust_be_complete_type、そうでなければコンパイラがエラーを報告します(char[-1]).githubを参照してください.
https://github.com/chenshuo/muduo/issues/301
shared_の使用ptr実現copy-on-write
前文postとtraverseのデッドロック問題、解決構想:読みはロックされていません.書くには臨界領域で、繰り返しロックを避ける必要があります. 書く時どのように渋滞しないで読みますか?shared_の使用ptrは占有されているかどうかを観測し,占有されている場合はコピーして書き込む. リードはどのように占有を表しますか?強引にシェアを増やすptrの参照数.
前文RequestとInventoryのデッドロック解決の考え方: printをprintAllの臨界領域外に除去する. printAllをリードとして、参照カウント表示占有量を増やします. add,removeを書き込み端とし,占有を検出した場合,再書き込みをコピーする.
第2章スレッド同期要約
スレッド同期の4つの原則は、重要度に基づいてソートされます.
はんぱつりょう
著者の陳碩氏が提案した反発量の使用原則:
副次的な原則:
再帰的なmutexとは、同じスレッドでmutexを繰り返しロックできることを意味します.
次のコードによるvectorの共有を考える
void post(const Foo& f)
{
lock_guard lg(mutex_);
vec.push_back(f);
}
void traverse()
{
lock_guard lg(mutex_);
for (auto it = vec.begin(); it != vec.end(); ++it)
{
it->doSomeThing();
}
}
mutexが再入力可能であれば、2つの関数を同時に実行できます.push_backは反復器の失効を引き起こす可能性がある.再読み込み不可の場合はdoSomeThingがpostを呼び出します.デッドロックが発生します.
1つの機能関数は、ロックされたバージョンとロックされていないバージョンに分けられ、2つのエラーが発生する可能性があります.
++エラー2++の場合、現在のスレッドがmutexにロックされていないことを保証するために、関数の先頭に断言を加えることができます.
// muduo::MutexLock
assert(mutex_.isLockedByThisThread());
++エラー1++単独ディスカッション
デッドロック
デッドロックインスタンス
class Request;
class Inventory
{
public:
void add(Request* req)
{
lock_guard lock(mutex_);
requests_.insert(req);
}
void remove(Request* req)// __attribute__ ((noinline))
{
lock_guard lock(mutex_);
requests_.erase(req);
}
void printAll() const;
private:
mutable mutex mutex_;
std::set requests_;
};
Inventory g_inventory;
class Request
{
public:
void process() // __attribute__ ((noinline))
{
lock_guard lock(mutex_);
g_inventory.add(this);
// ...
}
~Request()// __attribute__ ((noinline))
{
lock_guard lock(mutex_);
this_thread::sleep_for(chrono::milliseconds(1000));
g_inventory.remove(this);
}
void print()// const __attribute__ ((noinline))
{
lock_guard lock(mutex_);
// ...
}
private:
mutable mutex mutex_;
};
void Inventory::printAll() const
{
lock_guard lock(mutex_);
this_thread::sleep_for(chrono::milliseconds(1000));
for (std::set::const_iterator it = requests_.begin();
it != requests_.end();
++it)
{
(*it)->print();
}
printf("Inventory::printAll() unlocked
");
}
void threadFunc()
{
Request* req = new Request;
req->process();
delete req;
}
int main()
{
thread thread(threadFunc);
this_thread::sleep_for(chrono::milliseconds(500));
g_inventory.printAll();
thread.join();
}
上記のコードでは,InventoryのprintAll()はRequestの~Requset()より0.5 s遅れて起動し,両者とも1 s後に関数を呼び出し,自分のmutexを申請し,ループ待ちをもたらす.
じょうけんへんすう
mutexはロックにのみ使用され、条件変数はある条件の達成(ブール式が真である)を待つために使用され、学名管程(monitor).条件変数の使い方は1つだけです.
wait側の場合:
mutex mutex_;
deque dq;
condition_variable cv;
int number = 0;
void consumer()
{
unique_lock lock(mutex_);
cv.wait(lock, [] {return !dq.empty(); });
cout << dq.front() << endl;
dq.pop_front();
}
注意:
cv.wait(lock, [] {return !dq.empty(); });
mutexがロック解除され、現在のスレッドがブロックされます.lambdaがtrueを返すまで、ブロックが終了し、mutexが再ロックされます.while式を使用したのと同じです.unique_lockは汎用反発包装器であり、遅延ロック、ロックの期限付き試行、再帰ロック、所有権移転、および条件変数とともに使用することを可能にする.
signal/broadcastエンドの場合:
void productor()
{
lock_guard lock(mutex_);
dq.push_back(number++);
cv.notify_all();
}
条件変数の場合,下位層の同期原語は直接使用することは少なく,一般に上位層の同期措置を実現するために用いられる.カウントダウンはよく使われる同期手段で、2つの用途があります.
class CountDownLatch
{
public:
explicit CountDownLatch(int count) : cnt_(count) {};
void wait()
{
unique_lock lock(mutex_);
cv.wait(lock, [=] {return cnt_ == 0; });
}
void countDown()
{
lock_guard lock(mutex_);
cnt_--;
if (cnt_ == 0)
{
cv.notify_all();
}
}
private:
mutable mutex mutex_;
condition_variable cv;
int cnt_;
};
読み書きロックや信号量は使わないでください
初心者はたくさん読んで、少ないシーンを書くことに出会って、mutexをrwlockに置き換えて、正確ではありません:
信号量(Semaphore)は条件変数に取って代わることができ,著者らは経験がない.
スレッドセキュリティの単一モード実装
template
class Singleton : boost::noncopyable
{
public:
static T& instance()
{
pthread_once(&ponce_, &Singleton::init);
return *value_;
}
private:
Singleton();
~Singleton();
static void init()
{
value_ = new T();
::atexit(destroy);
}
static void destroy()
{
typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1];
delete value_;
}
private:
static pthread_once_t ponce_;
static T* value_;
};
template
pthread_once_t Singleton::ponce_ = PTHREAD_ONCE_INIT;
template
T* Singleton::value_ = NULL;
上記コードではプログラム終了時の破棄機能を実現するためにatexitを用いており,この関数の解釈は以下の通りである.
関数名:atexit機能:登録終了関数(main実行終了後に呼び出される関数)用法:int atexit(void(*func)(void);注意:atexit()に登録されている関数タイプは、パラメータを受け入れないvoid関数であり、exitがこれらの登録関数を呼び出す順序は、登録時の順序とは逆です.作者:Quinn 0918原文:https://blog.csdn.net/Quinn0918/article/details/70457370
pthread_once(&ponce_, &Singleton::init);
マルチスレッドがpthreadをどのように呼び出しても、登録されたinit関数を保証できます.once,initは一度だけ呼び出されます.
typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1];
このコードは、コンパイル期間において、Tタイプが不完全なタイプ(サイズ0)であるか否かを検査することを目的とする.本質はtypedefを用いてchar配列別名を定義することであり、Tタイプが完全なタイプである場合、char[1]配列別名はT_と定義されるmust_be_complete_type、そうでなければコンパイラがエラーを報告します(char[-1]).githubを参照してください.
https://github.com/chenshuo/muduo/issues/301
shared_の使用ptr実現copy-on-write
前文postとtraverseのデッドロック問題、解決構想:
mutex mutex_;
shared_ptr> g_foos;
void traverse()
{
shared_ptr> foos;
{
lock_guard lock(mutex_);
foos = g_foos;//
assert(!g_foos.unique());//
}
for (auto it = foos->begin(); it != foos->end(); ++it)
{
it->doSomeThing();
}
}
void post(const Foo& f)
{
lock_guard lock(mutex_);
if (!g_foos.unique())
{
g_foos.reset(new vector(*g_foos));// , copy
}
assert(g_foos.unique());//
g_foos->push_back(f);
}
前文RequestとInventoryのデッドロック解決の考え方:
class Request;
class Inventory
{
public:
Inventory()
: requests_(new RequestList)
{
}
void add(Request* req)
{
muduo::MutexLockGuard lock(mutex_);// ,
if (!requests_.unique())// ,
{
requests_.reset(new RequestList(*requests_));
printf("Inventory::add() copy the whole list
");
}
assert(requests_.unique());
requests_->insert(req);
}
void remove(Request* req) // __attribute__ ((noinline))
{
muduo::MutexLockGuard lock(mutex_);
if (!requests_.unique())
{
requests_.reset(new RequestList(*requests_));
printf("Inventory::remove() copy the whole list
");
}
assert(requests_.unique());
requests_->erase(req);
}
void printAll() const;
private:
typedef std::set RequestList;
typedef boost::shared_ptr RequestListPtr;
RequestListPtr getData() const
{
muduo::MutexLockGuard lock(mutex_);
return requests_;
}
mutable muduo::MutexLock mutex_;
RequestListPtr requests_;
};
Inventory g_inventory;
class Request
{
public:
Request()
: x_(0)
{
}
~Request() __attribute__ ((noinline))
{
muduo::MutexLockGuard lock(mutex_);
x_ = -1;
sleep(1);
g_inventory.remove(this);
}
void process() // __attribute__ ((noinline))
{
muduo::MutexLockGuard lock(mutex_);
g_inventory.add(this);
// ...
}
void print() const __attribute__ ((noinline))
{
muduo::MutexLockGuard lock(mutex_);
// ...
printf("print Request %p x=%d
", this, x_);
}
private:
mutable muduo::MutexLock mutex_;
int x_;
};
void Inventory::printAll() const
{
RequestListPtr requests = getData();// 1
sleep(1);
for (std::set::const_iterator it = requests->begin();
it != requests->end();
++it)
{
(*it)->print();//
}
}
void threadFunc()
{
Request* req = new Request;
req->process();
delete req;
}
int main()
{
muduo::Thread thread(threadFunc);
thread.start();
usleep(500*1000);
g_inventory.printAll();
thread.join();
}