“ Observer”の実装に関する問題パターン
-
06-07-2019 - |
質問
C ++およびSTLでObserverパターンを実装する際に興味深い問題に遭遇しました。この古典的な例を考えてみましょう:
class Observer {
public:
virtual void notify() = 0;
};
class Subject {
public:
void addObserver( Observer* );
void remObserver( Observer* );
private:
void notifyAll();
};
void Subject::notifyAll() {
for (all registered observers) { observer->notify(); }
}
この例は、デザインパターンに関するすべての書籍に記載されています。残念ながら、実際のシステムはより複雑であるため、最初の問題があります。一部のオブザーバーは、通知を受けたときにサブジェクトに他のオブザーバーを追加することにします。これにより、「for」が無効になります。ループと私が使用するすべてのイテレータ。解決策はかなり簡単です-登録済みのオブザーバーリストのスナップショットを作成し、スナップショットを反復処理します。新しいオブザーバーを追加してもスナップショットは無効にならないため、すべてが問題ないようです。しかし、ここで別の問題が発生します。オブザーバーは通知を受けたときに自分自身を破壊することにします。さらに悪いことに、1人のオブザーバーが他のすべてのオブザーバー(スクリプトから制御される)を破棄し、キューとスナップショットの両方を無効にすることもできます。割り当て解除されたポインターを反復処理していることに気付きました。
私の質問は、オブザーバーがお互いを殺すとき、どのように状況を処理するべきですか?すぐに使用できるパターンはありますか?私はいつも「オブザーバー」と思っていました。は世界で最も簡単な設計パターンですが、今では正しく実装するのはそれほど簡単ではないようです...
ご関心をお寄せいただきありがとうございます。意思決定の要約をしましょう:
[1]"しないでください" 申し訳ありませんが、必須です。オブザーバーはスクリプトから制御され、ガベージコレクションされます。ガベージコレクションを制御して割り当て解除を防ぐことはできません。
[2]" Use boost :: signal" 最も有望な決定ですが、プロジェクトに後押しを導入することはできません。そのような決定はプロジェクトリーダーのみが行う必要があります(プレイステーション);
[3]" Use shared__ptr" これにより、オブザーバーの割り当てが解除されなくなります。一部のサブシステムはメモリプールのクリーンアップに依存する場合があるため、shared_ptrを使用できないと思います。
[4]"オブザーバーの割り当て解除を延期する 通知中にオブザーバーを削除するためにキューに入れ、2番目のサイクルを使用してそれらを削除します。残念ながら、割り当て解除を防ぐことはできません。そのため、実際には「アダプタ」のリストを保持しながら、ある種の「アダプタ」でオブザーバをラップするトリックを使用します。デストラクタでは、オブザーバはアダプタの割り当てを解除し、2回目のサイクルで空のアダプタを破棄します。
p.s。すべての投稿を要約するために質問を編集しても大丈夫ですか?私はStackOverflowの初心者です...
解決
非常に興味深い問題。
これを試してください:
- remObserverを変更して、エントリを単に削除する(およびリストイテレータを無効にする)のではなく、エントリをnullにします。
-
notifyAllループを次のように変更します。
for(登録されているすべてのオブザーバー){if(オブザーバー)オブザーバー-> notify(); }
-
notifyAllの最後に別のループを追加して、オブザーバーリストからすべてのnullエントリを削除します
他のヒント
個人的に、 boost :: signals 私のオブザーバーを実装する。確認する必要がありますが、上記のシナリオを処理していると思います(編集済み:見つかった、"いつ切断が発生する可能性があります" )。実装を簡素化し、カスタムクラスの作成に依存しません。
class Subject {
public:
boost::signals::connection addObserver( const boost::function<void ()>& func )
{ return sig.connect(func); }
private:
boost::signal<void ()> sig;
void notifyAll() { sig(); }
};
void some_func() { /* impl */ }
int main() {
Subject foo;
boost::signals::connection c = foo.addObserver(boost::bind(&some_func));
c.disconnect(); // remove yourself.
}
ある男が医者に行き、「ドクター、このように腕を上げると、ひどく痛い!」と言います。医師は、「それをしないでください」と言います。
最も簡単な解決策は、チームと協力して、それを行わないように指示することです。オブザーバーが「本当に必要」な場合自分自身またはすべてのオブザーバーを殺すために、通知が終了したときにアクションをスケジュールします。または、さらに良いことに、remObserver関数を変更して、通知プロセスが発生しているかどうかを確認し、すべてが完了したときの削除を待ち行列に入れます。
アイデアT.E.Dのバリエーションです。既に提示されています。
remObserverがエントリをすぐに削除する代わりにnullにできる限り、notifyAllを次のように実装できます。
void Subject::notifyAll()
{
list<Observer*>::iterator i = m_Observers.begin();
while(i != m_Observers.end())
{
Observer* observer = *i;
if(observer)
{
observer->notify();
++i;
}
else
{
i = m_Observers.erase(i);
}
}
}
これにより、2回目のクリーンアップループが不要になります。ただし、特定のnotify()呼び出しがそれ自体またはリストの前にあるオブザーバーの削除をトリガーした場合、リスト要素の実際の削除は次のnotifyAll()まで延期されます。ただし、リスト上で機能する関数が適切な場合に適切な注意を払ってnullエントリをチェックする限り、これは問題になりません。
問題は所有権の問題です。スマートポインター、たとえば boost :: shared_ptr
および boost :: weak_ptr
クラスを使用して、オブザーバーの存続期間を&quot; de-allocation&quot。 ;。
この問題にはいくつかの解決策があります:
-
boost :: signal
を使用すると、オブジェクトが破壊されたときに自動的に接続を削除できます。ただし、スレッドセーフに細心の注意を払う必要があります。 - オブザーバーの管理には
boost :: weak_ptr
またはtr1 :: weak_ptr
を使用し、boost :: shared_ptr
またはtr1を使用します。 :shared_ptr
オブザーバは自己参照します-参照カウントは オブジェクトの無効化に役立ちます。weak_ptrはオブジェクトが存在するかどうかを知らせます。 -
何らかのイベントループで実行している場合、各オブザーバーが 同じ呼び出しで自分自身を破壊するか、自分自身またはその他を追加します。仕事を延期する、つまり
SomeObserver::notify() { main_loop.post(boost::bind(&SomeObserver::someMember,this)); }
for
ループでリンクリストを使用するのはどうですか?
プログラムがマルチスレッドの場合、ここでロックを使用する必要があります。
とにかく、あなたの説明から、問題は並行性(マルチトラッド)ではなく、Observer :: notify()呼び出しによって引き起こされた突然変異であるようです。この場合、ベクトルを使用し、イテレータではなくインデックスを介してそれを走査することで問題を解決できます。
for(int i = 0; i < observers.size(); ++i)
observers[i]->notify();
current
( end
イテレータに初期化された)という名前のメンバーイテレータを作成する方法について。その後
void remObserver(Observer* obs)
{
list<Observer*>::iterator i = observers.find(obs);
if (i == current) { ++current; }
observers.erase(i);
}
void notifyAll()
{
current = observers.begin();
while (current != observers.end())
{
// it's important that current is incremented before notify is called
Observer* obs = *current++;
obs->notify();
}
}
削除に対して回復力があり(たとえば、前述のようにnullアウト)、追加(たとえば、追加)を処理できるノーティファイアのコンテナに対して、強力なイテレータを定義および使用します
一方、通知中にコンテナのconstを保持することを強制する場合は、notifyAllと反復対象のコンテナをconstとして宣言します。
コレクションをコピーしているため、これは少し遅くなりますが、私はそれも簡単だと思います。
class Subject {
public:
void addObserver(Observer*);
void remObserver(Observer*);
private:
void notifyAll();
std::set<Observer*> observers;
};
void Subject::addObserver(Observer* o) {
observers.insert(o);
}
void Subject::remObserver(Observer* o) {
observers.erase(o);
}
void Subject::notifyAll() {
std::set<Observer*> copy(observers);
std::set<Observer*>::iterator it = copy.begin();
while (it != copy.end()) {
if (observers.find(*it) != observers.end())
(*it)->notify();
++it;
}
}
反復中にオブザーバーが削除されることを避けることはできません。
オブザーバは、 notify()
関数を呼び出そうとしている WHILE
からも削除できます。
したがって、 try / catch メカニズムが必要だと思います。
ロックは、オブザーバーのセットのコピー中にオブザーバーセットが変更されないようにすることです
lock(observers)
set<Observer> os = observers.copy();
unlock(observers)
for (Observer o: os) {
try { o.notify() }
catch (Exception e) {
print "notification of "+o+"failed:"+e
}
}
数か月前にこの記事に出くわしたとき、この問題の解決策を探していました。ソリューションについて考えるようになり、ブーストやスマートポインターなどに依存しないソリューションがあると思います。
要するに、ソリューションのスケッチは次のとおりです。
- オブザーバーは、サブジェクトが関心を登録するためのキーを持つシングルトンです。シングルトンであるため、常に存在します。
- 各サブジェクトは、共通の基本クラスから派生しています。基本クラスには、派生クラスに実装する必要がある抽象仮想関数Notify(...)と、削除時にオブザーバー(常に到達可能な)から削除するデストラクターがあります。
- オブザーバー自体の内部で、Notify(...)の進行中にDetach(...)が呼び出されると、切り離されたサブジェクトはすべてリストになります。
- オブザーバーでNotify(...)が呼び出されると、サブジェクトリストの一時コピーが作成されます。それを反復処理するときに、最近切り離されたものと比較します。ターゲットがその上にない場合、ターゲットでNotify(...)が呼び出されます。それ以外の場合はスキップされます。 オブザーバーの
- Notify(...)は、カスケードコールを処理する深さを追跡します(AはB、C、Dに通知し、D.Notify(...)はNotify(...)呼び出しをトリガーします) Eなど)
これはうまくいくようです。ソリューションは、ソースとともにWeb こちらに投稿されています。コード。これは比較的新しい設計なので、フィードバックは大歓迎です。
完全なオブザーバークラスを作成しました。テストが完了したら、それを含めます。
しかし、あなたの質問に対する私の答えは、ケースを処理することです!
私のバージョンでは、通知ループの内部で通知ループをトリガーできます(すぐに実行され、これを深さ優先再帰と見なします)が、Observableクラスが通知が実行されていることと深さ。
オブザーバーが削除されると、そのデストラクタはすべてのオブザーバブルに破壊についてサブスクライブしていることを通知します。オブザーバーがいるという通知ループにない場合、そのオブザーバブルはstd :: list&lt; pair&lt; Observer *、int&gt;&gt;から削除されます。そのイベントがループ内にある場合、リスト内のエントリは無効になり、通知カウンターがゼロになったときに実行されるキューにコマンドがプッシュされます。そのコマンドは無効化されたエントリを削除します。
したがって、基本的に、安全に削除できない場合(通知を行うエントリを保持するイテレータが存在する可能性があるため)、削除する代わりにエントリを無効にします。
すべての同時ノーウェイトシステムと同様に、ルールはロックアウトされていない場合でもケースを処理しますが、その場合は作業をキューに入れ、ロックを解除したときにロックを保持しているユーザーが作業を行います。