我有一个向客户提供Subscribe(Observer*)
和Unsubscribe(Observer*)
的主题。 Subject在自己的线程中运行(在订阅的Observers上调用Notify()
)并且互斥锁保护其内部观察者列表。
我希望客户端代码 - 我无法控制 - 能够在取消订阅后安全地删除Observer。如何实现这一目标?
修改
下面是一些说明性代码。问题是如何防止在“运行时出现问题”评论时发生取消订阅。然后我可以回拨一个已删除的对象。或者,如果我持有互斥体而不是制作副本,我可能会使某些客户陷入僵局。
#include <set>
#include <functional>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
using namespace std;
using namespace boost;
class Observer
{
public:
void Notify() {}
};
class Subject
{
public:
Subject() : t(bind(&Subject::Run, this))
{
}
void Subscribe(Observer* o)
{
mutex::scoped_lock l(m);
observers.insert(o);
}
void Unsubscribe(Observer* o)
{
mutex::scoped_lock l(m);
observers.erase(o);
}
void Run()
{
for (;;)
{
WaitForSomethingInterestingToHappen();
set<Observer*> notifyList;
{
mutex::scoped_lock l(m);
notifyList = observers;
}
// Problem here
for_each(notifyList.begin(), notifyList.end(),
mem_fun(&Observer::Notify));
}
}
private:
set<Observer*> observers;
thread t;
mutex m;
};
修改
由于死锁风险,我无法在持有互斥锁时通知观察者。这种情况最明显的方式 - 客户端从Notify内部调用Subscribe或Unsubscribe - 可以通过使互斥锁递归来轻松解决。更加阴险的是不同线程间歇性死锁的风险。
我处于多线程环境中,因此在线程执行的任何时候,它通常会保存一系列锁L1,L2,... Ln。另一个线程将保持锁K1,K2,... Km。正确编写的客户端将确保不同的线程始终以相同的顺序获取锁。但是当客户端与我的Subject的互斥体交互时 - 称之为X - 这个策略将被破坏:调用订阅/取消订阅以L1,L2,... Ln,X的顺序获取锁。从我的主题线程调用通知获取锁定X,K1,K2,... Km的顺序。如果Li或Kj中的任何一个可以在任何呼叫路径上重合,则客户端会遇到间歇性死锁,几乎没有调试它的可能性。由于我不控制客户端代码,我不能这样做。
答案 0 :(得分:7)
Unsubscribe()应该是同步的,因此在Observer被保证不再在Subject列表中之前它不会返回。这是安全地做到这一点的唯一方法。
ETA(将我的评论移至答案):
由于时间似乎不是问题,因此在通知每个观察者之间取出并释放互斥锁。您将无法以现在的方式使用for_each,并且您必须检查迭代器以确保它仍然有效。
for ( ... )
{
take mutex
check iterator validity
notify
release mutex
}
这将做你想要的。
答案 1 :(得分:3)
您可以更改Subscribe()和Unsubscribe()的签名吗?用像shared_ptr&lt; Observer&gt;这样的东西替换Observer *会让事情变得更容易。
编辑:通过上面的“更容易”替换“简单”。 有关如何难以“正确”的示例,请参阅Boost.Signals和adopted的历史记录 - 但尚未分发Boost.Signals2(以前的Boost.ThreadSafeSignals)库。
答案 2 :(得分:1)
嗯......我真的不明白你的问题,因为如果一个客户端调用Unsubscribe你应该能够让客户端删除它(它不被你使用)。但是,如果由于某种原因,一旦客户端取消订阅观察者,您就无法关闭关系,您可以添加“主题”以安全地删除观察者,或者只是让客户发出信号表明他们对观察者不再感兴趣
重新思考编辑:好的,现在我想我明白你的问题是什么。我认为解决问题的最佳方法是执行以下操作:
鉴于取消订阅操作将阻止互斥锁重置有效标志(并且该特定Observer将不再在您的线程中使用),代码是线程安全的,并且客户端可以尽快删除任何观察者因为取消订阅已经退回。
答案 3 :(得分:1)
这样的事情会令人满意吗?虽然在通知时取消订阅观察者仍然是不安全的,因为你需要一个像你提到的界面(据我所知)。
Subscribe(Observer *x)
{
mutex.lock();
// add x to the list
mutex.unlock();
}
Unsubscribe(Observer *x)
{
mutex.lock();
while (!ok_to_delete)
cond.wait(mutex);
// remove x from list
mutex.unlock();
}
NotifyLoop()
{
while (true) {
// wait for something to trigger a notify
mutex.lock();
ok_to_delete = false;
// build a list of observers to notify
mutex.unlock();
// notify all observers from the list saved earlier
mutex.lock();
ok_to_delete = true;
cond.notify_all();
mutex.unlock();
}
}
如果您希望能够在Notify() - (在客户端IMO上做出错误的设计决定......)中取消订阅(),您可以将通知程序线程的线程ID添加到您的数据结构。在Unsubscribe函数中,您可以根据当前线程的id检查该线程ID(大多数线程库提供此功能 - 例如.pthread_self)。如果它们相同,则无需等待条件变量即可继续。
注意:如果客户端负责删除观察者,这意味着你会遇到Notify回调内部的情况,你将取消订阅并删除观察者,但仍在执行带有该指针的垃圾。这是客户必须注意的事情,并且只在Notify()的末尾删除它。
答案 4 :(得分:1)
不是让客户端获得“SafeToDelete”通知,而是为他们提供IsSubscribed(Observer *)方法。然后客户端代码变为:
subject.Unsubscribe( obsever );l
while( subject.IsSubscribed( observer ) ) {
sleep_some_short_time; // OS specific sleep stuff
}
delete observer;
这不是太麻烦。
答案 5 :(得分:1)
您可以在CSubject类型中创建“删除队列”。删除Observer时,可以调用pSubject-&gt; QueueForDelete(pObserver)。然后当主题线程在通知之间时,它可以安全地从队列中删除观察者。
答案 6 :(得分:1)
“理想”解决方案涉及使用shared_ptr
和weak_ptr
。但是,为了保持通用性,它还必须考虑Subject
在某些Observer
之前被删除的问题(是的,这也可能发生)。
class Subject {
public:
void Subscribe(std::weak_ptr<Observer> o);
void Unsubscribe(std::weak_ptr<Observer> o);
private:
std::mutex mutex;
std::set< std::weak_ptr<Observer> > observers;
};
class Observer: boost::noncopyable {
public:
~Observer();
void Notify();
private:
std::mutex;
std::weak_ptr<Subject> subject;
};
通过这种结构,我们创建了一个循环图,但明智地使用了weak_ptr
,这样就可以在没有协调的情况下销毁Observer
和Subject
。
注意:为简单起见,我假设Observer
一次只能观察一个Subject
,但可以轻松观察多个主题。
现在,您似乎陷入了不安全的内存管理。这是一个非常困难的情况,你可以想象。在这种情况下,我建议进行一项实验:异步Unsubscribe
。或者至少,对Unsubscribe
的调用将与外部同步,但可以异步实现。
这个想法很简单:我们将使用事件队列来实现同步。那就是:
Unsubscribe
的调用在队列中发布了一个事件(有效负载Observer*
),然后等待Subject
线程处理了Unsubscribe
个事件时,它会唤醒等待的线程您可以使用busy-waiting或条件变量,除非性能另有规定,否则我会建议条件变量。
注意:此解决方案完全无法解决Subject
过早死亡的问题。
答案 7 :(得分:0)
我认为如果不是非常优雅的话,这就会成功:
class Subject {
public:
Subject() : t(bind(&Subject::Run, this)),m_key(0) { }
void Subscribe(Observer* o) {
mutex::scoped_lock l(m);
InternalObserver io( o );
boost::shared_ptr<InternalObserver> sp(&io);
observers.insert(pair<int,boost::shared_ptr<InternalObserver>> (MakeKey(o),sp));
}
void Unsubscribe(Observer* o) {
mutex::scoped_lock l(m);
observers.find( MakeKey(o) )->second->exists = false; }
void WaitForSomethingInterestingToHappen() {}
void Run()
{
for (;;)
{
WaitForSomethingInterestingToHappen();
for( unsigned int i = 0; i < observers.size(); ++ i )
{
mutex::scoped_lock l(m);
if( observers[i]->exists )
{
mem_fun(&Observer::Notify);//needs changing
}
else
{
observers.erase(i);
--i;
}
}
}
}
private:
int MakeKey(Observer* o) {
return ++m_key;//needs changeing, sha of the object?
}
class InternalObserver {
public:
InternalObserver(Observer* o) : m_o( o ), exists( true ) {}
Observer* m_o;
bool exists;
};
map< int, boost::shared_ptr<InternalObserver> > observers;
thread t;
mutex m;
int m_key;
};
答案 8 :(得分:0)
使用密钥observers
将map
更改为Observer*
,并为Observer
的值包装。包装器包含volatile
布尔值,用于指示Observer
是否有效。在subscribe
方法中,包装器对象是在有效状态下创建的。在unsubscribe
方法中,包装器标记为无效。在包装而不是实际观察者上调用Notify
。如果有效(仍然订阅),包装器将在实际观察者上调用Notify
#include <map>
#include <functional>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
using namespace std;
using namespace boost;
class Observer
{
public:
void Notify() {}
};
class ObserverWrapper : public Observer
{
public:
Observer* wrappee;
volatile bool valid;
ObserverWrapper(Observer* o)
{
wrappee = o;
valid = true;
}
void Notify()
{
if (valid) wrappee->Notify();
}
}
class Subject
{
public:
Subject() : t(bind(&Subject::Run, this))
{
}
void Subscribe(Observer* o)
{
mutex::scoped_lock l(m);
boost::shared_ptr<ObserverWrapper> sptr(new ObserverWrapper(o));
observers.insert(pair<Observer*, sptr));
}
void Unsubscribe(Observer* o)
{
mutex::scoped_lock l(m);
observers.find(o)->second->valid = false;
observers.erase(o);
}
void Run()
{
for (;;)
{
WaitForSomethingInterestingToHappen();
vector<ObserverWrapper*> notifyList;
{
mutex::scoped_lock l(m);
boost::copy(observers | boost::adaptors::map_values, std::back_inserter(notifyList));
}
// Should be no problem here
for_each(notifyList.begin(), notifyList.end(),
mem_fun(&ObserverWrapper::Notify));
}
}
private:
map<Observer*, ObserverWrapper*> observers;
thread t;
mutex m;
};