2013-01-17 7 views
13

Sto provando a scrivere una classe Subject sicura dallo observer pattern. Voglio sapere se si utilizza weak_ptr è il modo migliore per conservare IObserver casi in modo tale che:Schema di osservatore utilizzando weak_ptr

  • Non è possibile utilizzare un'istanza IObserver dopo che è stato free'd.
  • La classe Subject non è valida per i riferimenti IObserver che dovrebbero essere liberati (lapsed listener problem).
  • La classe Subject deve essere thread-safe.

Sfortunatamente, i nostri standard di codifica dicono che non è consentito utilizzare boost. Credo di essere stato una persona cattiva in una vita precedente. Fortunatamente, sono autorizzato a utilizzare C++ 11 (ciò che viene fornito con Visual Studio 2012).

Ecco un esempio di classe Observer.

// Observer interface that supports notify() method 
class IObserver 
{ 
public: 
    virtual void notify() const = 0; 
    virtual ~IObserver() {} 
}; 

// Concrete observer implementation that prints a message 
class Observer : public IObserver 
{ 
public: 
    Observer(const std::string& message) : m_message(message){} 

    void notify() const { 
     printf("%s\r\n", m_message.c_str()); 
    } 

private: 
    std::string m_message; 
}; 

E qui è la classe Subject.

// Subject which registers observers and notifies them as needed. 
class Subject 
{ 
public: 
    // Use shared_ptr to guarantee the observer is valid right now 
    void registerObserver(const std::shared_ptr<IObserver>& o) 
    { 
     std::lock_guard<std::mutex> guard(m_observersMutex); 
     m_observers.push_back(o); 
    } 

    void unregisterObserver(const std::shared_ptr<IObserver>& o) 
    { 
     std::lock_guard<std::mutex> guard(m_observersMutex); 
     // Code to remove the observer from m_observersMutex 
    } 

    // This is a method that is run in its own thread that notifies observers of some event 
    void doNotify() 
    { 
     std::lock_guard<std::mutex> guard(m_observersMutex); 
     // Notify any valid observers of events. 
     std::for_each(m_observers.cbegin(), m_observers.cend(), 
      [](const std::weak_ptr<IObserver>& o) 
     { 
      auto observer = o.lock(); 
      if (observer) { 
       observer->notify(); 
      } 
     }); 

     // Remove any dead observers. These are ones which have expired(). 
     m_observers.erase(std::remove_if(m_observers.begin(), m_observers.end(), 
      [](const std::weak_ptr<IObserver>& o) 
     { 
      return o.expired(); 
     }), m_observers.end()); 

    } 


private: 
    std::vector<std::weak_ptr<IObserver>> m_observers; 
    std::mutex m_observersMutex; 
}; 

Ecco alcuni codice che esercita Subject:

int main(int argc, wchar_t* argv[]) 
{ 

    Subject subject; 
    auto observerHello = std::make_shared<Observer>("Hello world"); 
    subject.registerObserver(observerHello); 
    { 
     // Create a scope to show unregistration. 
     auto observerBye = std::make_shared<Observer>("Good bye"); 
     subject.registerObserver(observerBye); 

     subject.doNotify(); 
    } 
    printf("%s\r\n", "Observer good bye is now be destructed"); 
    subject.doNotify(); 
    return 0; 
} 

È il mio utilizzo di weak_ptr thread-safe? Da qui https://stackoverflow.com/a/2160422/1517648 I penso che sia.

Si tratta di un modo legittimo per risolvere il problema dell'ascolto scaduto?

+0

non 2012 ha gamma base per i cicli? perché stai usando for_each? (irrilevante alla domanda, heh) – David

+0

È solo un'abitudine, non c'è altra ragione se non quella a cui sono abituato. – Steve

risposta

11

Sarei un po 'sospettoso del tuo doNotify - Supponiamo che qualcosa in un osservatore che fai fuoco aggiunga o rimuova gli osservatori? - accadono cose brutte (inclusi arresti anomali). O bloccando l'azione di un altro thread, che blocca cercando di aggiungere un osservatore? - Le cose brutte accadono (deadlock!)

Questo è difficile da risolvere. Fondamentalmente, è un problema con la rientranza.

Mai e poi mai lasciare il controllo del codice quando si tiene un lucchetto. Tenere un lucchetto mentre si chiama una richiamata è un no-no.

Quindi, come minimo:

Blocco quindi copiare l'elenco quindi sbloccare. Mentre si esegue questa copia, è possibile rimuovere anche gli osservatori scaduti (sia dall'originale che dalla copia, elenco).

Quindi sparare agli osservatori dall'elenco copiato.

Questo lascia alcuni problemi irrisolti. Come il fatto che rimuovere un osservatore non garantisce che non verrà chiamato in futuro! Significa solo che alla fine non sarà chiamato.

Quanto è importante dipende da come si utilizza l'ascolto.

Un approccio che potrebbe funzionare è una coda di attività che include eventi aggiungi/rimuovi/notifica/killthread (rendendo l'attività di killthread in coda rende l'interruzione molto meno fastidiosa). Ora tutta la sincronizzazione è in coda.Se non si è in grado di scrivere una coda senza blocco di blocco, il codice di notifica può semplicemente bloccare, std::move la coda, sbloccare, quindi procedere per eseguirla. Oppure puoi scrivere una coda in modo tale che blocchi pop finché non c'è qualcosa da leggere, e push non blocca.

una "copia e broadcast" quick-and-dirty potrebbe essere simile a questo:

std::vector<std::shared_ptr<IObserver>> targets; 
{ 
    std::lock_guard<std::mutex> guard(m_observersMutex); 
    m_observers.erase(std::remove_if(m_observers.begin(), m_observers.end(), 
     [&targets](const std::weak_ptr<IObserver>& o) 
    { 
     std::shared_ptr<IObserver> ptr = o.lock(); 
     if (ptr) { 
     targets.push_back(ptr); 
     return false; 
     } else { 
     return true; 
     } 
    }), m_observers.end()); 
} 

for(auto& target:targets) { 
    target->notify(); 
} 
+0

Questo è davvero utile, grazie :) Per ora andrò con la copia e la trasmissione perché ricevere un evento dopo aver rimosso un osservatore non è un grosso problema. Dovendo però leggere su code senza blocco non bloccanti. – Steve