muduo之當解構函式遇見執行緒安全
一、當解構函式遇到多執行緒
當一個物件能被多個執行緒同時看到時,那麼物件的銷燬時機就會變得模糊不清,可能出現多種競態條件:
① 在即將析構一個物件時,如何知道此時是否有別的執行緒正在執行該物件的成員函式?
② 如何保證在執行成員函式期間,物件不會被另一個執行緒析構?
③ 在呼叫某個物件的成員函式之前,如何得知該物件還活著?它的解構函式會不會碰巧執行到一半?
二、物件的銷燬太難
2.1 作為資料成員的mutex不能保護析構
mutex只能保證函式一個接著一個地執行。下面從程式碼開始分析:
儘管執行緒A在銷燬物件x之後把x設為NULL,儘管執行緒B在呼叫x之前檢查了指標x地值,但是還是無法避免一種race condition:
1.執行緒A呼叫delete x,執行到(1)free internal state,執行緒A此刻已經持有了mutex_,即將繼續向下執行。
2.x並沒有被執行緒A設為NULL,因此執行緒B能通過if(x),進入x->update(),將阻塞在(2)處
3.之後因為解構函式銷燬x地同時也將成員變數mutex_給銷燬,因此(2)將會發生難以預料的結果(可能會永遠阻塞在(2)處,也可能會進入“臨界區”,然後core dump,或者發生更糟糕的情況)
結論:作為class資料成員的mutex_只能同步本class的其他資料成員的讀和寫,不能保護安全的析構。
2.2 執行緒不安全的Observer模式
class Subject
{
public:
virtual ~Subject() {};
void attach(Observer* obsvr)
{
m_observers.push_back(obsvr);
}
void remove(Observer* obsvr)
{
m_observers.remove(obsvr);
}
void notify(const std::string &msg)
{
for(Observer* obs:m_observers)
obs->Update(); [1]
}
private:
std::vector<Observer* > m_observers; //觀察者集合
};
執行緒A:當Subject通知每一個Observer時(程式碼[1]),它無法得知Observer物件是否還活著?
執行緒B:觀察者Observer正在呼叫解構函式
此時執行緒A中,該Observer物件處於將死未死的狀態,呼叫Update()函式,core dump恐怕是最幸運的結果。
三、一個萬能的解決方案
上文講解了各種各樣的錯誤,解決方案的核心要點是:析構過程不需要保護,所有人都用不到的東西一定是垃圾,垃圾自動回收的原理是“引用計數為0時,該物件就是垃圾,需要銷燬”
舉例:下面使用weak_ptr / share_ptr + Mutex實現執行緒安全的Observer模式
既然通過weak_ptr的expired函式可以探查物件的生死,那麼Observer的競態問題就很容易解決,只要讓Observer儲存weak_ptr<Observer>即可。
class Subject
{
public:
virtual ~Subject() {};
void attach(weak_ptr<Observer> obsvr);
// void remove(weak_ptr<Observer> obsvr); //不需要它,會自動移除
void notify(const std::string &msg)
{
//加鎖,任何一個時刻,保證只有一個執行緒去喚醒所有Observer
MutexLockGuard lock(mutex_);
std::vector<weak_ptr<Observer>>::iterator iter = m_observers.begin();
for(iter != m_observers.end())
{
//嘗試提升,即獲得weak_ptr對應的shared_ptr
shared_ptr<Observer> obj(iter->lock());
if(obj) //成功,返回一個非空的shared_ptr
{
//提升成功,現在的引用計數至少為2(想想為什麼?)
obj->Opdate(); //沒有競態條件
++iter;
}
else //失敗,返回一個空的shared_ptr
{
//物件已經銷燬,從容器中拿掉weak_ptr
it = m_observers.erase(iter);
}
}
}
private:
mutable MutexLock mutex_;
std::vector<weak_ptr<Observer>> m_observers; //觀察者集合
};
分析:在呼叫Update()之前,先用it->lock()判斷該物件是否存在,如果存在就呼叫Update();如果不存在,就刪除it。
總的說就是,使用weak_ptr可以探查物件的生死。以便控制物件的生命週期。