Linux多線(xiàn)程服務(wù)端編程筆記 第一章

本文解決如下幾個(gè)問(wèn)題:

  1. 如何實(shí)現(xiàn)一個(gè)線(xiàn)程安全的容器,以及這個(gè)線(xiàn)程安全的容器什么時(shí)候是不安全的;
  2. 構(gòu)造函數(shù)中,為保證線(xiàn)程安全禁止做哪些事情。
  3. 析構(gòu)函數(shù)中不宜使用鎖的原因。
  4. 使用指針時(shí)該如何判斷指針是否還存活?
  5. 使用鎖會(huì)降低程序的效率,使得并行的程序串行化,如何減少鎖爭(zhēng)用造成的延遲。
  6. shared_ptr的使用技巧與坑;
  7. 對(duì)象池中對(duì)象關(guān)系的探討:如何降低對(duì)象之間的相互依賴(lài)。
  8. 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)程安全禁止做哪些事情。

  1. 不能在構(gòu)造函數(shù)中注冊(cè)回調(diào)。
  2. 不要在構(gòu)造函數(shù)中把this傳遞給跨線(xiàn)程對(duì)象;
  3. 即使在構(gòu)造函數(shù)最后一行也不行。

解釋?zhuān)?/p>

  1. 在構(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)用了成員方法,未定義行為。

  1. 與上一點(diǎn)相同。感覺(jué)含義很類(lèi)似。
  2. 最后一行也不行是因?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的使用技巧與坑

坑:

  1. 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ā)生拷貝行為)

  1. shared_ptr的拷貝代價(jià)比指針要大。
    解釋?zhuān)寒吘惯€要保存引用計(jì)數(shù)等變量,修改引用計(jì)數(shù)等行為。(建議使用引用傳遞)

  2. 不能同時(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;

使用技巧:

  1. 作為函數(shù)參數(shù)時(shí),建議使用const reference傳遞;
  2. 在創(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ì)象。
  3. 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)程;
  4. 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ù)一樣了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.C和C++的區(qū)別?C++的特性?面向?qū)ο缶幊痰暮锰帲?答:c++在c的基礎(chǔ)上增添類(lèi),C是一個(gè)結(jié)構(gòu)化語(yǔ)言,它的重...
    杰倫哎呦哎呦閱讀 10,007評(píng)論 0 45
  • C#、Java、python和go等語(yǔ)言中都有垃圾自動(dòng)回收機(jī)制,在對(duì)象失去引用的時(shí)候自動(dòng)回收,而且基本上沒(méi)有指針的...
    StormZhu閱讀 3,876評(píng)論 1 15
  • 1. (1) 簡(jiǎn)述智能指針的原理;(2)c++中常用的智能指針有哪些?(3)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的智能指針。 簡(jiǎn)述智能指針...
    編程半島閱讀 3,093評(píng)論 0 17
  • 原作者:Babu_Abdulsalam 本文翻譯自CodeProject,轉(zhuǎn)載請(qǐng)注明出處。 引入### Ooops...
    卡巴拉的樹(shù)閱讀 30,363評(píng)論 13 74
  • 清晨,六點(diǎn),一輛單車(chē),寒風(fēng)中,有點(diǎn)冷。 離開(kāi)溫暖的被窩,努力,奔跑。 離開(kāi)溫暖的被窩,為自己的人生奮斗。 離開(kāi)偷...
    魔女_f6c6閱讀 498評(píng)論 0 0

友情鏈接更多精彩內(nèi)容