ミューテックスによって保護されたデータの同時読み取りがかなり一般的である一方、書き込みはまれであるコードにいくつかのスポットがあることがわかりました。私たちの測定では、単純なミューテックスを使用すると、そのデータを読み取るコードのパフォーマンスが著しく低下すると言われているようです。したがって、必要なのは複数読み取り/単一書き込みミューテックスです。私はこれがより単純なプリミティブの上に構築できることを知っていますが、これを試す前に、既存の知識を求めたいです。
より単純な同期プリミティブから複数読み取り/単一書き込みロックを構築する承認された方法は何ですか?
私はそれを作る方法を考えていますが、私が(おそらく間違って)思いついたものに偏っていない答えを持っています。 (注:私が期待するのは、それを行う方法の説明であり、おそらく疑似コードであり、本格的な実装ではありません。確かに自分でコードを書くことができます。)
警告:
これには適切なパフォーマンスが必要です。 (私が考えているのは、アクセスごとに2つのロック/ロック解除操作を必要とすることです。今では十分ではないかもしれませんが、代わりにそれらの多くを必要とすることは不合理に思えます。)
一般に、読み取りは多数ありますが、書き込みは読み取りよりも重要であり、パフォーマンスに敏感です。読者は作家を飢えさせてはなりません。
かなり古いコンパイラー(GCC 4.1.2)を備えたかなり古い組み込みプラットフォーム(VxWorks 5.5の専有バリアント)、およびPOSIXが完全に実装されていないため、POSIXに依存するBoostのほとんどの部分を除き、ブースト1.52そのプラットフォームで。基本的に利用可能なロックプリミティブは、いくつかの種類のセマフォ(バイナリ、カウントなど)であり、その上に既にmutex、条件変数、およびモニターを作成しています。
これは、IA32、シングルコアです。
同期プリミティブとしてmutexとcondition_variableのみを持っているようです。そのため、ここではリーダーライターロックを作成します。 1つのmutex、2つのcondition_variable、および3つの整数を使用します。
readers - readers in the cv readerQ plus the reading reader
writers - writers in cv writerQ plus the writing writer
active_writers - the writer currently writing. can only be 1 or 0.
このようにして読者を飢えさせます。書きたいライターが複数いる場合、すべてのライターが書き終わるまで、リーダーは読む機会を得ることができません。これは、後の読者がwriters
変数を確認する必要があるためです。同時に、active_writers
変数は、一度に1人のライターのみが書き込みできることを保証します。
class RWLock {
public:
RWLock()
: shared()
, readerQ(), writerQ()
, active_readers(0), waiting_writers(0), active_writers(0)
{}
void ReadLock() {
std::unique_lock<std::mutex> lk(shared);
while( waiting_writers != 0 )
readerQ.wait(lk);
++active_readers;
lk.unlock();
}
void ReadUnlock() {
std::unique_lock<std::mutex> lk(shared);
--active_readers;
lk.unlock();
writerQ.notify_one();
}
void WriteLock() {
std::unique_lock<std::mutex> lk(shared);
++waiting_writers;
while( active_readers != 0 || active_writers != 0 )
writerQ.wait(lk);
++active_writers;
lk.unlock();
}
void WriteUnlock() {
std::unique_lock<std::mutex> lk(shared);
--waiting_writers;
--active_writers;
if(waiting_writers > 0)
writerQ.notify_one();
else
readerQ.notify_all();
lk.unlock();
}
private:
std::mutex shared;
std::condition_variable readerQ;
std::condition_variable writerQ;
int active_readers;
int waiting_writers;
int active_writers;
};
一見したところ、私は この答え をAlexander Terekhovが導入したのと同じアルゴリズムとして認識したと思った。しかし、それを研究した後、私はそれが欠陥があると信じています。 2人のライターが_m_exclusive_cond
_を同時に待機することは可能です。これらのライターの1つが起動して排他ロックを取得すると、unlock
に_exclusive_waiting_blocked = false
_が設定され、ミューテックスが一貫性のない状態に設定されます。その後、ミューテックスはホース接続される可能性があります。
N2406 、最初に提案された_std::shared_mutex
_には、部分的な実装が含まれます。これは、更新された構文で以下で繰り返されます。
_class shared_mutex
{
mutex mut_;
condition_variable gate1_;
condition_variable gate2_;
unsigned state_;
static const unsigned write_entered_ = 1U << (sizeof(unsigned)*CHAR_BIT - 1);
static const unsigned n_readers_ = ~write_entered_;
public:
shared_mutex() : state_(0) {}
// Exclusive ownership
void lock();
bool try_lock();
void unlock();
// Shared ownership
void lock_shared();
bool try_lock_shared();
void unlock_shared();
};
// Exclusive ownership
void
shared_mutex::lock()
{
unique_lock<mutex> lk(mut_);
while (state_ & write_entered_)
gate1_.wait(lk);
state_ |= write_entered_;
while (state_ & n_readers_)
gate2_.wait(lk);
}
bool
shared_mutex::try_lock()
{
unique_lock<mutex> lk(mut_, try_to_lock);
if (lk.owns_lock() && state_ == 0)
{
state_ = write_entered_;
return true;
}
return false;
}
void
shared_mutex::unlock()
{
{
lock_guard<mutex> _(mut_);
state_ = 0;
}
gate1_.notify_all();
}
// Shared ownership
void
shared_mutex::lock_shared()
{
unique_lock<mutex> lk(mut_);
while ((state_ & write_entered_) || (state_ & n_readers_) == n_readers_)
gate1_.wait(lk);
unsigned num_readers = (state_ & n_readers_) + 1;
state_ &= ~n_readers_;
state_ |= num_readers;
}
bool
shared_mutex::try_lock_shared()
{
unique_lock<mutex> lk(mut_, try_to_lock);
unsigned num_readers = state_ & n_readers_;
if (lk.owns_lock() && !(state_ & write_entered_) && num_readers != n_readers_)
{
++num_readers;
state_ &= ~n_readers_;
state_ |= num_readers;
return true;
}
return false;
}
void
shared_mutex::unlock_shared()
{
lock_guard<mutex> _(mut_);
unsigned num_readers = (state_ & n_readers_) - 1;
state_ &= ~n_readers_;
state_ |= num_readers;
if (state_ & write_entered_)
{
if (num_readers == 0)
gate2_.notify_one();
}
else
{
if (num_readers == n_readers_ - 1)
gate1_.notify_one();
}
}
_
このアルゴリズムは、Alexander Terekhovの古いニュースグループ投稿から派生しています。読者も作家も飢えません。
2つの「ゲート」、_gate1_
_と_gate2_
_があります。リーダーとライターは_gate1_
_を渡す必要があり、そうしようとするとブロックされる可能性があります。リーダーが_gate1_
_を超えると、mutexを読み取りロックします。所有者を持つリーダーの最大数がない限り、また作家が_gate1_
_を超えていない限り、読者は_gate1_
_を超えて取得できます。
一度に1人のライターのみが_gate1_
_を超えることができます。そして、たとえ読者が所有権を持っているとしても、作家は_gate1_
_を過ぎてしまうことができます。しかし、一度_gate1_
_を過ぎても、作家にはまだ所有権がありません。最初に_gate2_
_を超えなければなりません。ライターは、所有権を持つすべてのリーダーがそれを放棄するまで、_gate2_
_を超えることはできません。ライターが_gate1_
_で待機している間、新しいリーダーは_gate2_
_を超えることができないことを思い出してください。また、ライターが_gate1_
_で待機している間に、新しいライターが_gate2_
_を超えることもできません。
リーダーとライターの両方が_gate1_
_でブロックされ、それを通過するために(ほぼ)同じ要件が課されるという特性は、このアルゴリズムをリーダーとライターの両方に公平にし、どちらも飢えさせないことです。
ミューテックスの「状態」は、特定の状態の変化に対するアトミックの(最適化としての)部分的な使用が可能性(つまり、競合しない「高速パス」)であることを示唆するために、単一のWordに意図的に保持されます。ただし、その最適化はここでは実証されていません。 1つの例として、ライタースレッドが_state_
_を0から_write_entered
_にアトミックに変更できた場合、_mut_
_をブロックまたはロック/ロック解除することなくロックを取得できます。また、unlock()
はアトミックストアで実装できます。これらの最適化は、この簡単な説明で説明するよりもはるかに難しいため、ここでは示していません。
ミューテックスで保護されたデータの同時読み取りはかなり一般的ですが、書き込みはまれです
ser-space RC の理想的なシナリオのように思えます。
URCUは、Linuxカーネルの同等物に似ており、他の用途の中でも特に、リーダーライターロックの代わりを提供します。この類似性は、RCUアップデーターと直接同期しないリーダーでも継続されるため、RCU読み取り側のコードパスが非常に高速になり、さらにRCUリーダーがRCUアップデーターと同時に実行されている場合でも有益な前進を行うことができます。
あなたが助けるためにできるいくつかの良いトリックがあります。
まず、良好なパフォーマンス。 VxWorksは、非常に優れたコンテキスト切り替え時間で有名です。使用するロックソリューションにセマフォが関係する可能性があります。セマフォ(複数)を使用することを恐れず、VxWorksで最適化されており、高速コンテキスト切り替え時間は、多くのセマフォ状態の評価などによるパフォーマンスの低下を最小限に抑えるのに役立ちます。
また、VxWorkのセマフォの上に単純に階層化されるPOSIXセマフォの使用を忘れます。 VxWorksは、ネイティブカウント、バイナリおよびミューテックスセマフォを提供します。適したものを使用すると、少し速くなります。バイナリのものは時々非常に便利です。何度も投稿され、値1を超えることはありません。
第二に、書き込みは読み取りよりも重要。 VxWorksでこの種の要件があり、セマフォを使用してアクセスを制御しているとき、タスク優先度を使用して、どのタスクがより重要で、リソースに最初にアクセスする必要があるかを示しました。これは非常にうまく機能します。文字通りVxWorksのすべては、すべてのデバイスドライバーなどを含む、他のタスクと同様のタスク(まあ、スレッド)です。
VxWorksは、優先順位の逆転(Linus Torvaldsが嫌いな種類)も解決します。したがって、セマフォを使用してロックを実装する場合、優先順位の高いライターをブロックしている場合は、OSスケジューラを使用して優先順位の低いリーダーを無効にすることができます。それははるかに単純なコードにつながる可能性があり、あなたもOSを最大限に活用しています。
潜在的な解決策は、単一のVxWorksカウントセマフォでリソースを保護し、リーダーの数に等しい値に初期化することです。リーダーが読み取りを行うたびにセマフォを取得します(カウントを1ずつ減らします。読み取りが完了するたびにセマフォをポストし、カウントを1増やします。ライターが書き込みを行うたびにセマフォn(n =読者の数)回、完了時にn回投稿最後に、ライターのタスクをどの読者よりも優先度を高くし、OSの高速コンテキスト切り替え時間と優先度の反転に依存します。
Linuxではなく、ハードリアルタイムOSでプログラミングしていることに注意してください。ネイティブVxWorksセマフォの取得/投稿には、Linuxでの同様の行為と同じ量のランタイムは関係しませんが、最近はLinuxでもかなり良いです(私は最近PREEMPT_RTを使用しています)。 VxWorksスケジューラとすべてのデバイスドライバーは、動作に依存できます。必要に応じて、ライタータスクをシステム全体で最も優先順位を高くし、すべてのデバイスドライバーよりも高くすることもできます。
物事を助けるために、各スレッドが何をしているのかも考えてください。 VxWorksでは、タスクがFPUを使用しているかどうかを示すことができます。 pthread_createの代わりにネイティブVxWorks TaskSpawnルーチンを使用している場合、これを指定する機会があります。つまり、スレッド/タスクが浮動小数点演算を実行しておらず、TaskSpawnの呼び出しでそのように言った場合、スケジューラは/を保持することを気にしないため、コンテキスト切り替え時間がさらに速くなります。 FPU状態を復元します。
これは、開発中のプラットフォームで最良のソリューションになる合理的なチャンスです。他のプラットフォームで一般的に見られる代替の(そしておそらくよりエレガントな)ソリューションを再作成するために余分なコードを大量に導入することなく、OSの長所(高速セマフォ、高速コンテキスト切り替え時間)を再生します。
第三に、古いGCCと古いBoostでスタック。基本的に、WindRiverに電話をかけ、アップグレードの購入について話し合うことについての低価値の提案以外には、私はそこを助けることができません。個人的には、VxWorksのプログラミングをしているときに、POSIXではなくVxWorkのネイティブAPIを使用しました。それでは、コードの移植性はそれほど高くありませんが、高速になりました。とにかくPOSIXはネイティブAPIの上のレイヤーにすぎず、常に速度が低下します。
とはいえ、POSIXカウントおよびミューテックスセマフォは、VxWorkのネイティブカウントおよびミューテックスセマフォに非常に似ています。これはおそらく、POSIXレイヤーがそれほど厚くないことを意味します。
VxWorksのプログラミングに関する一般的な注意事項
デバッグ私は常に、Solarisで利用可能な開発ツール(Tornado)を使用しようとしました。これは、これまで出会った中で最高のマルチスレッドデバッグ環境です。どうして?システム内のスレッド/タスクごとに1つずつ、複数のデバッグセッションを開始できます。スレッドごとにデバッグウィンドウが表示され、個々のデバッグウィンドウを個別にデバッグします。ブロッキングウィンドウをブロックすると、そのデバッグウィンドウがブロックされます。マウスフォーカスを別のデバッグウィンドウに移動し、ブロックを解放する操作をステップオーバーして、最初のウィンドウがそのステップを完了するのを確認します。
たくさんのデバッグウィンドウが表示されますが、マルチスレッドのものをデバッグする最良の方法です。非常に複雑なものを書いたり、問題を見つけたりするのが簡単になりました。各スレッドが何をしているのかを簡単かつ強力に制御できるため、アプリケーションのさまざまな動的相互作用を簡単に探索できます。
皮肉なことに、TornadoのWindowsバージョンではこれができませんでした。他の退屈な古いIDEなどのVisual Studioなど)と同じように、システムごとに1つの悲惨な単一デバッグウィンドウ。最近のIDEでさえ、SolarisのTornadoに匹敵するものを見たことがありません。マルチスレッドデバッグ用。
HardDrivesリーダーとライターがディスク上のファイルを使用している場合、VxWorks 5.5はかなり古いと考えてください。 NCQのようなものはサポートされません。この場合、提案された解決策(上記の概要)は、単一のミューテックスセマフォを使用して、複数のリーダーがディスクのさまざまな部分を読み取るのに苦労して互いにつまずくのを防ぐ方が適切です。リーダーが何をしているのかによって異なりますが、ファイルから連続したデータを読み取っている場合は、読み取り/書き込みヘッドがディスク表面を行き来することを回避できます(非常に遅い)。
私の場合、このトリックを使用して、ネットワークインターフェース全体のトラフィックをシェーピングしていました。各タスクは異なる種類のデータを送信し、タスクの優先度はネットワーク上のデータの優先度を反映していました。非常にエレガントで、メッセージは断片化されませんでしたが、重要なメッセージは利用可能な帯域幅の大部分を占めました。
いつものように、最適なソリューションは詳細に依存します。 読み取り/書き込みスピンロックはあなたが探しているものかもしれません 、しかし上で提案されたread-copy-updateのような他のアプローチは解決策かもしれません-古い組み込みプラットフォームでは余分なメモリが使用されます問題になるかもしれません。まれな書き込みでは、タスクシステムを使用して作業を調整し、そのデータ構造からの読み取りがない場合にのみ書き込みが発生するようにしますが、これはアルゴリズムに依存します。
セマフォとミューテックスに基づくこのアルゴリズムの1つについては、 Concurrent Control with Readers and Writers; PJ Courtois、F. Heymans、DLパルナス;MBLE Research Laboratory;ブリュッセル、ベルギー .
これは、Boostヘッダーに基づく簡略化された回答です(Boostを承認済みの方法と呼びます)。条件変数とミューテックスのみが必要です。 Windowsプリミティブを使用して書き直しました。なぜなら、それらは説明的で非常に単純だと思うからです。しかし、これを疑似コードと見なしています。
これは非常に単純なソリューションであり、mutexのアップグレードやtry_lock()操作などをサポートしていません。必要に応じて追加できます。また、厳密に必要ではない割り込みを無効にするなど、いくつかのフリルも削除しました。
また、boost\thread\pthread\shared_mutex.hpp
(これはそれに基づいている)をチェックアウトする価値があります。人間が読むことができます。
class SharedMutex {
CRITICAL_SECTION m_state_mutex;
CONDITION_VARIABLE m_shared_cond;
CONDITION_VARIABLE m_exclusive_cond;
size_t shared_count;
bool exclusive;
// This causes write blocks to prevent further read blocks
bool exclusive_wait_blocked;
SharedMutex() : shared_count(0), exclusive(false)
{
InitializeConditionVariable (m_shared_cond);
InitializeConditionVariable (m_exclusive_cond);
InitializeCriticalSection (m_state_mutex);
}
~SharedMutex()
{
DeleteCriticalSection (&m_state_mutex);
DeleteConditionVariable (&m_exclusive_cond);
DeleteConditionVariable (&m_shared_cond);
}
// Write lock
void lock(void)
{
EnterCriticalSection (&m_state_mutex);
while (shared_count > 0 || exclusive)
{
exclusive_waiting_blocked = true;
SleepConditionVariableCS (&m_exclusive_cond, &m_state_mutex, INFINITE)
}
// This thread now 'owns' the mutex
exclusive = true;
LeaveCriticalSection (&m_state_mutex);
}
void unlock(void)
{
EnterCriticalSection (&m_state_mutex);
exclusive = false;
exclusive_waiting_blocked = false;
LeaveCriticalSection (&m_state_mutex);
WakeConditionVariable (&m_exclusive_cond);
WakeAllConditionVariable (&m_shared_cond);
}
// Read lock
void lock_shared(void)
{
EnterCriticalSection (&m_state_mutex);
while (exclusive || exclusive_waiting_blocked)
{
SleepConditionVariableCS (&m_shared_cond, m_state_mutex, INFINITE);
}
++shared_count;
LeaveCriticalSection (&m_state_mutex);
}
void unlock_shared(void)
{
EnterCriticalSection (&m_state_mutex);
--shared_count;
if (shared_count == 0)
{
exclusive_waiting_blocked = false;
LeaveCriticalSection (&m_state_mutex);
WakeConditionVariable (&m_exclusive_cond);
WakeAllConditionVariable (&m_shared_cond);
}
else
{
LeaveCriticalSection (&m_state_mutex);
}
}
};
さて、このアルゴリズムの動作については混乱がありますので、その仕組みを説明します。
書き込みロック中-リーダーとライターの両方がブロックされます。
書き込みロックの最後-リーダースレッドと1つのライタースレッドがraceで、どちらが開始するかを確認します。
読み取りロック中-ライターはブロックされます。リーダーもブロックされます、そしてライターがブロックされた場合のみ。
最終読み取りロックのリリース時-リーダースレッドと1つのライタースレッドは、どちらが開始するかを確認するためにraceになります。
このcouldにより、通知中にm_shared_cond
の前にプロセッサがm_exclusive_cond
スレッドに頻繁にコンテキストを切り替えると、リーダーはライターを飢えさせます。しかし、この問題は理論上のもので、Boostのアルゴリズムなので実用的ではないと思います。
Microsoftが.NETソースコードを公開したので、 ReaderWRiterLockSlim 実装を確認できます。
それらが使用するより基本的なプリミティブが利用可能かどうかはわかりませんが、それらのいくつかは.NETライブラリの一部であり、そのコードも利用可能です。
マイクロソフトは、ロックメカニズムのパフォーマンスの改善にかなりの時間を費やしてきたため、これは良い出発点となります。