本文解決如下幾個(gè)問(wèn)題:
- 如何實(shí)現(xiàn)一個(gè)線(xiàn)程安全的容器,以及這個(gè)線(xiàn)程安全的容器什么時(shí)候是不安全的;
- 構(gòu)造函數(shù)中,為保證線(xiàn)程安全禁止做哪些事情。
- 析構(gòu)函數(shù)中不宜使用鎖的原因。
- 使用指針時(shí)該如何判斷指針是否還存活?
- 使用鎖會(huì)降低程序的效率,使得并行的程序串行化,如何減少鎖爭(zhēng)用造成的延遲。
- shared_ptr的使用技巧與坑;
- 對(duì)象池中對(duì)象關(guān)系的探討:如何降低對(duì)象之間的相互依賴(lài)。
- std::bind與std::function的簡(jiǎn)單理解。
1. stl中的容器大部分都不是線(xiàn)程安全的,如何將其變?yōu)榫€(xiàn)程安全呢?
解決方案:使用鎖保護(hù)數(shù)據(jù)成員:
class Array {
private:
mutable std::mutex lock; //注意mutable關(guān)鍵字;
std::vector<int> data;
}
成員函數(shù)的實(shí)現(xiàn)使用鎖保護(hù)data成員。
問(wèn)題:C++中指針訪(fǎng)問(wèn)Array時(shí)是不能保證線(xiàn)程安全的。
理由:C++的析構(gòu)函數(shù)的存在,使得其他線(xiàn)程delete掉Array*時(shí),其他線(xiàn)程還阻塞在lock中,析構(gòu)函數(shù)完成,lock就不存在了,阻塞在lock的線(xiàn)程就出現(xiàn)了未定義行為。
2. 構(gòu)造函數(shù)中,為保證線(xiàn)程安全禁止做哪些事情。
- 不能在構(gòu)造函數(shù)中注冊(cè)回調(diào)。
- 不要在構(gòu)造函數(shù)中把this傳遞給跨線(xiàn)程對(duì)象;
- 即使在構(gòu)造函數(shù)最后一行也不行。
解釋?zhuān)?/p>
- 在構(gòu)造函數(shù)中注冊(cè)回調(diào)的含義是:將自己的指針保存到容器Observable中,一旦有事件發(fā)生,就會(huì)調(diào)用自己的方法。
Foo(Observable* s) {
s->register(this);
}
this傳遞出去后,會(huì)導(dǎo)致有可能當(dāng)前對(duì)象沒(méi)有構(gòu)造完成就調(diào)用了成員方法,未定義行為。
- 與上一點(diǎn)相同。感覺(jué)含義很類(lèi)似。
- 最后一行也不行是因?yàn)?,?dāng)前類(lèi)可能是父類(lèi),子類(lèi)的對(duì)象依然沒(méi)有初始化完成,導(dǎo)致未定義行為。
3. 析構(gòu)函數(shù)中不宜使用鎖的原因。
(1)調(diào)用析構(gòu)函數(shù)的時(shí)候,正常邏輯來(lái)說(shuō)這個(gè)對(duì)象已經(jīng)沒(méi)有其他線(xiàn)程在使用了,用鎖也沒(méi)有效果;
(2)即使使用了鎖,析構(gòu)函數(shù)搶到了鎖,其他線(xiàn)程還在等待這個(gè)鎖,析構(gòu)函數(shù)中鎖被析構(gòu)掉了,其他線(xiàn)程就是未定義行為。
4. 使用指針時(shí)該如何判斷指針是否還存活?
例子:以觀(guān)察者模式為例,observer對(duì)象注冊(cè)自己到Observable,后者保存有前者的指針,一旦某個(gè)事件發(fā)生,Observable就通過(guò)observer指針調(diào)用其成員方法。多線(xiàn)程情況下,Observable無(wú)法得知當(dāng)前調(diào)用的observer指針是否還有效,即使使用鎖也不行。
方案:需要一種方法能告訴Observable,observer指針是否存活的方法。什么都不做是不可能實(shí)現(xiàn)的,需要額外一個(gè)變量來(lái)表示變量是否存活,可以理解為是指針通過(guò)一個(gè)代理來(lái)訪(fǎng)問(wèn)實(shí)際內(nèi)存,代理掌握了實(shí)際內(nèi)存是否被釋放的消息。
實(shí)現(xiàn):本質(zhì)就是shared_ptr與weak_ptr的實(shí)現(xiàn)。如果使用weak_ptr保存指針,可以清楚的知道指針是否存活:如果weak_ptr可以轉(zhuǎn)化為shared_ptr,證明指針還有效,否則無(wú)效。
weak_ptr不增加引用計(jì)數(shù),weak_ptr對(duì)象只能由shared_ptr/weak_ptr賦值構(gòu)造而來(lái)。
不打算決定對(duì)象的生死,就使用weak_ptr管理對(duì)象指針;否則使用shared_ptr
vector<weak_ptr<Observer>> x;
lock.lock();
for(auto xx : x) {
shared_ptr<Observer> obj(xx->lock());
if(obj) {
obj->update();
}
else {
x.erase(xx);
}
繼續(xù):即使能判斷指針是否存活,即不會(huì)存在使用已經(jīng)銷(xiāo)毀或者正在銷(xiāo)毀的指針了!但是不代表沒(méi)有其他問(wèn)題:
(1)鎖爭(zhēng)用造成的延時(shí);
(2)死鎖。
5. 使用鎖會(huì)降低程序的效率,使得并行的程序串行化,如何減少鎖爭(zhēng)用造成的延遲。
鎖爭(zhēng)用:訪(fǎng)問(wèn)需要加鎖的數(shù)據(jù)成員的代碼都需要加鎖,使得比較簡(jiǎn)單的函數(shù)也需要等待較長(zhǎng)時(shí)間。
解決1:解決鎖爭(zhēng)用的方法是:盡量減少臨界區(qū)的大小;
解決方法1:local copy的方式。(適用于拷貝代價(jià)不大的對(duì)象)
讀操作,臨界區(qū)內(nèi)拷貝出來(lái),臨界區(qū)外使用副本讀取;
寫(xiě)操作,臨界區(qū)外定義副本,完成要完成的操作,臨界區(qū)內(nèi)直接賦值或者swap;
寫(xiě)操作,臨界區(qū)內(nèi)拷貝出來(lái),臨界區(qū)外副本操作,臨界區(qū)內(nèi)swap;這樣會(huì)有問(wèn)題,可能會(huì)覆蓋其他線(xiàn)程的修改。
寫(xiě)操作如果操作的是shared_ptr,可能會(huì)造成shared_ptr保存對(duì)象的析構(gòu)操作(原來(lái)shared_ptr對(duì)象被賦值了,且引用計(jì)數(shù)為1),此時(shí)析構(gòu)操作也是在臨界區(qū)內(nèi)。
寫(xiě)操作時(shí)析構(gòu)移除臨界區(qū):臨界區(qū)外定義副本,完成要完成的操作,臨界區(qū)內(nèi)swap;(此時(shí)析構(gòu)移到了臨時(shí)對(duì)象的身上)
6. shared_ptr的使用技巧與坑
坑:
- shared_ptr會(huì)延長(zhǎng)對(duì)象的生命周期。
解釋?zhuān)耗承┖瘮?shù)實(shí)參采用非引用類(lèi)型shared_ptr類(lèi)型,調(diào)用這個(gè)函數(shù)的時(shí)候就會(huì)發(fā)生shared_ptr的拷貝操作,使得對(duì)象指針的引用計(jì)數(shù)值變大。如果這個(gè)函數(shù)返回一個(gè)對(duì)象,這個(gè)對(duì)象也中也存在這個(gè)shared_ptr的指針,那么shared_ptr對(duì)象的聲明周期就被延長(zhǎng)了。
例子:std::bind函數(shù),基本作用是,為一個(gè)函數(shù)指針提供默認(rèn)參數(shù)。其實(shí)參就會(huì)被拷貝一份出來(lái)。(模板參數(shù),不論什么類(lèi)型都會(huì)發(fā)生拷貝行為)
shared_ptr的拷貝代價(jià)比指針要大。
解釋?zhuān)寒吘惯€要保存引用計(jì)數(shù)等變量,修改引用計(jì)數(shù)等行為。(建議使用引用傳遞)不能同時(shí)使用兩個(gè)shared_ptr,容易引起誤會(huì);
類(lèi)內(nèi)(成員函數(shù))使用shared_ptr<this>與類(lèi)外使用shared_ptr同時(shí)使用時(shí),會(huì)造成析構(gòu)兩次的問(wèn)題。
解釋?zhuān)侯?lèi)內(nèi)部使用share_ptr<this>的需求可以使用:shared_from_this() 代替this;
使用技巧:
- 作為函數(shù)參數(shù)時(shí),建議使用const reference傳遞;
- 在創(chuàng)建shared_ptr對(duì)象時(shí),可以手動(dòng)指定析構(gòu)函數(shù),這樣可以保證可以跨dll來(lái)刪除。
解釋?zhuān)簑indows下的進(jìn)程會(huì)有好幾個(gè)堆,每個(gè)dll都會(huì)有一個(gè)堆,一個(gè)堆里申請(qǐng)的需要在這個(gè)堆釋放,所以存在跨模塊釋放的問(wèn)題;shared_ptr通過(guò)指定析構(gòu)函數(shù),使得釋放時(shí),可以釋放對(duì)應(yīng)堆的對(duì)象。 - shared_ptr的析構(gòu)如果可能發(fā)生在關(guān)鍵進(jìn)程,可以用一個(gè)專(zhuān)門(mén)的線(xiàn)程來(lái)處理析構(gòu),使用BlockQueue<shared_ptr>來(lái)轉(zhuǎn)移對(duì)象到析構(gòu)線(xiàn)程;
- ower持有指向child的shared_ptr,child持有指向ower的weak_ptr;
解釋?zhuān)簅wer可以決定對(duì)象的生死,child只負(fù)責(zé)使用,不符合對(duì)象的生死。
7. 對(duì)象池中對(duì)象關(guān)系的探討:如何降低對(duì)象之間的相互依賴(lài)。
場(chǎng)景:A類(lèi)中包含了B類(lèi)對(duì)象,對(duì)象池的話(huà)就是A類(lèi)中包含了很多B類(lèi)對(duì)象。B類(lèi)對(duì)象可以是暫存在A(yíng)類(lèi)中的,用于回調(diào);也可能是被A類(lèi)所使用。
(1) 需求:A類(lèi)中的B類(lèi)對(duì)象如果不使用了及時(shí)釋放掉,以節(jié)省內(nèi)存。
解決方案:不使用了的概念是沒(méi)有線(xiàn)程在使用了,可以使用指針來(lái)保存B類(lèi)對(duì)象,shared_ptr來(lái)保存,使得引用計(jì)數(shù)為0時(shí)就釋放掉。顯然,使用shared_ptr的話(huà),對(duì)象永遠(yuǎn)不會(huì)被釋放掉。所以使用weak_ptr來(lái)保存。
class Item {};
class Factory {
private:
std::map<std::string, weak_ptr<Item>> data_;
mutable std::mutex lock_;
public:
shared_ptr<Item> get(const std::string& key); //使用shared_ptr作為返回值,因?yàn)槌鋈ナ褂玫膶?duì)象認(rèn)為不能隨便釋放掉。
};
get方法的實(shí)現(xiàn)就相當(dāng)較為簡(jiǎn)單了:
shared_ptr<Item> Factory::get(const string& key) {
shared_ptr<Item> ret;
lock_.lock();
auto itemptr = data_[key].lock(); //即使不存在,itemptr也是合理的weak_ptr
if(! itemptr) {
ret.reset(new Item());
data_[key] = ret; //第一,weak_ptr只能由shared_ptr/weak_ptr賦值而來(lái)。 第二,兩次查找map,效率不高,可以使用引用保存第一次查找的結(jié)果。
}
lock_.unlock();
return ret;
}
shared_ptr與weak_ptr使得對(duì)象可以被及時(shí)釋放。
(2)需求:資源的及時(shí)釋放,保存有weak_ptr的對(duì)象也要及時(shí)清理掉內(nèi)存,如何處理。
創(chuàng)建了一個(gè)Item給外部使用,保存在data_中以便不要重復(fù)創(chuàng)建;但是外部用完了,對(duì)象就自動(dòng)銷(xiāo)毀了,但是Factory還保存著資源的weak_ptr,沒(méi)有意義了。要清理掉。
即:對(duì)象的析構(gòu)不僅僅需要釋放自己,還要處理保存有自己weak_ptr的對(duì)象
解決方案:使用shared_ptr定制的析構(gòu)函數(shù)來(lái)處理。
void deleteItem(Item* item, Factory* factory) {
factory->deleteItem(item->key()); //key是從Item類(lèi)中獲取。
delete item;
}
(3)問(wèn)題:定制析構(gòu)需要只能有一個(gè)參數(shù),且參數(shù)應(yīng)該是傳到shared_ptr中的對(duì)象指針,那現(xiàn)在要處理factory,多了一個(gè)參數(shù),要咋處理呢?
解決方案:std::bind函數(shù)縮減函數(shù)參數(shù)
auto deleter = std::bind(deleteItem, _1, this); //普通函數(shù)作為第一個(gè)參數(shù),不需要使用&,靜態(tài)成員函數(shù)與成員函數(shù)使用時(shí)需要。
這里的this只是一個(gè)例子,代表是Factory* 就可以了,因?yàn)榍懊鎠hared_ptr在Factory中構(gòu)造而來(lái),所以使用this。
(4)指針是不能隨便出現(xiàn)的,出現(xiàn)了就會(huì)存在內(nèi)存釋放問(wèn)題,也存在指針是否是野指針的問(wèn)題。上面的deleteItem函數(shù)不合理。
解決:由于在函數(shù)內(nèi)部無(wú)法判斷factory指向的對(duì)象是否還存在,所以不能直接調(diào)用。上面第四點(diǎn)說(shuō)明了判斷指針是否存在可以使用shared_ptr與weak_ptr來(lái)決定。
至于用哪個(gè),得看Factory*是不是在這里必須存在,顯然,這里只是清理Factory內(nèi)部數(shù)據(jù),如果Factory對(duì)象不在了,就不清理就好了。所以使用weak_ptr;
所以:
void deleteItem(Item* pItem, weak_ptr<Factory> pFactory) {
share_ptr<Factory> pFactoryShare = pFactory.lock();
if(pFactoryShare) {
pFactoryShare->deleteItem(pItem->getKey()); //pFactoryShare指向的對(duì)象一定存在。
}
delete pItem;
}
(5)如何將this變?yōu)閟hared_ptr<this>或者weak_ptr<this>,以方便在類(lèi)成員函數(shù)內(nèi)部調(diào)用shared_ptr<Factory>為參數(shù)的函數(shù)?
上面第6點(diǎn)說(shuō)明了,內(nèi)部不可以直接使用shared_ptr<this>,以防止外部也使用shared_ptr<Factory>,造成兩次析構(gòu)的出現(xiàn)。
所以可以使用如下方式來(lái)實(shí)現(xiàn):(思路固定。背下來(lái)就好)
class Factory : public std::enable_shared_from_this<Factory> { //必須繼承這個(gè)類(lèi);
//類(lèi)內(nèi)部需要使用shared_ptr<this>的地方,使用shared_from_this()來(lái)代替。
//類(lèi)內(nèi)部需要使用weak_ptr<this>的地方,使用std::weak_ptr<Factory>(shared_from_this())就好了(就是使用shared_from_this()來(lái)生成了下weak_ptr())
};
通過(guò)相互使用weak_ptr,Item與Factory誰(shuí)也不管誰(shuí),誰(shuí)掛了也無(wú)所謂。
8. std::bind與std::function的簡(jiǎn)單理解。
使用起來(lái)比較簡(jiǎn)單,不再介紹,只介紹簡(jiǎn)單的理解。
std::function可以理解為可以統(tǒng)一函數(shù)格式,可以為函數(shù)重新命令。什么普通函數(shù),類(lèi)靜態(tài)函數(shù),類(lèi)成員函數(shù),經(jīng)過(guò)function都變成了普通函數(shù)的格式,隨便調(diào)用。(類(lèi)成員函數(shù)的調(diào)用需要加上類(lèi)對(duì)象指針)
std::bind不僅可以實(shí)現(xiàn)std::function函數(shù)的作用,還可以實(shí)現(xiàn)減少參數(shù)數(shù)量,添加默認(rèn)參數(shù)等功能。
std::bind一個(gè)很常見(jiàn)的使用方式:為類(lèi)靜態(tài)函數(shù)和成員函數(shù)指定別名,簡(jiǎn)化類(lèi)靜態(tài)函數(shù)的調(diào)用方式。為成員函數(shù)提前加好類(lèi)對(duì)象指針在第一個(gè)參數(shù),后邊再調(diào)用這個(gè)函數(shù)的時(shí)候,直接和調(diào)用類(lèi)成員函數(shù)一樣了。