C++技巧(多線程下的返回值優(yōu)化陷阱)

字號(hào):

“函數(shù)的返回值優(yōu)化”是我們對(duì)程序的一個(gè)常見優(yōu)化手段。只要可能,我們都應(yīng)該返回對(duì)象的有效引用,而不是重新生成一個(gè)臨時(shí)對(duì)象。但是,也許這種想法在多線程里需要更仔細(xì)的斟酌一下。
    Examda從一個(gè)簡單例子講起:
    template
    class FdMap
    {
    std::vector vec_;
    public:
    void Set(int fd,const T & v){
    if(fd >= 0){
    if(fd < vec_.size()){
    vec_[fd] = v;
    }else{
    vec_.resize(fd + 1);
    vec_[fd] = v;
    }
    }
    }
    T Get(int fd) const{
    if(fd >= 0 && fd < vec_.size())
    return vec_[fd];
    return T();
    }
    };
    這里FdMap是一個(gè)把int映射到T對(duì)象的類型,考試大采用std::vector作為底層容器。它所做的工作很簡單:在需要的時(shí)候擴(kuò)充容器vec_;如果訪問超過了范圍,返回T的默認(rèn)值。
    如果FdMap要在多線程下工作,考試大那么我們需要加一點(diǎn)代碼,進(jìn)行必要的同步:
    template
    class FdMap
    {
    std::vector vec_;
    Mutex mutex_; //鎖同步對(duì)象
    public:
    void Set(int fd,const T & v){
    if(fd >= 0){
    Guard g(mutex_) //加鎖
    if(fd < vec_.size()){
    vec_[fd] = v;
    }else{
    vec_.resize(fd + 1);
    vec_[fd] = v;
    }
    }
    }
    T Get(int fd) const{
    if(fd >= 0){
    Guard g(mutex_) //加鎖
    if(fd < vec_.size()){
    return vec_[fd];
    }
    return T();
    }
    };
    其中Mutex是任意線程同步類型;Guard是對(duì)應(yīng)的范圍保衛(wèi)類型,在構(gòu)造的時(shí)候鎖住mutex_,析構(gòu)的時(shí)候解鎖。到目前為止,F(xiàn)dMap類非常完美,沒有任何問題。
    在實(shí)際應(yīng)用中,我們可能把FdMap用于關(guān)聯(lián)socket fd和對(duì)應(yīng)的客戶端連接對(duì)象(例如ClientData),于是可能的實(shí)例化是FdMap。
    不過有經(jīng)驗(yàn)的程序員馬上會(huì)看出,直接在FdMap里存儲(chǔ)ClientData指針是不行的。如果某個(gè)線程Get到了一個(gè)fd的ClientData指針,而另一個(gè)線程卻試圖關(guān)閉這個(gè)fd連接,那么ClientData對(duì)象是釋放掉,還是繼續(xù)有效?如果釋放了ClientData對(duì)象,那么前一個(gè)線程隨后的訪問就會(huì)失效;如果繼續(xù)保留ClientData對(duì)象,那么誰也不知道該在何時(shí)釋放它了,就會(huì)內(nèi)存泄漏。
    正確的做法是,在FdMap里存儲(chǔ)ClientData的智能指針,那么哪個(gè)線程都不需要特意釋放ClientData對(duì)象,智能指針會(huì)在適當(dāng)?shù)臅r(shí)候處理。為了保險(xiǎn),我們決定使用boost::shared_ptr,因?yàn)榇蠹叶颊J(rèn)為它是正確的。
    OK,我們有了一個(gè)設(shè)計(jì)完美,運(yùn)行正確的FdMap,完整的實(shí)例化是:
    FdMap >
    終于有一天,我們需要優(yōu)化代碼,于是我們重新審視上面的代碼。Get函數(shù)!是的,它的返回值是否可以優(yōu)化?因?yàn)閷?duì)于智能指針,即使是拷貝構(gòu)造函數(shù)都是昂貴的,能夠減少一個(gè)臨時(shí)對(duì)象的構(gòu)造,對(duì)于我們的確太誘人了。
    但是有一個(gè)明顯的問題:當(dāng)fd不在范圍內(nèi)的時(shí)候,返回誰的引用?且看下面的實(shí)現(xiàn)。
    下面是我們的“優(yōu)化”:
    const T & Get(int fd) const{
    static const T DEF = T();
    if(fd >= 0){
    Guard g(mutex_) //加鎖
    if(fd < vec_.size()){
    return vec_[fd];
    }
    return DEF;
    }
    通過增加一個(gè)局部靜態(tài)常量DEF,解決了返回默認(rèn)引用的問題。如果fd在范圍內(nèi),那么返回vec_里的對(duì)象引用,是沒有問題的。
    但是當(dāng)我再次運(yùn)行這個(gè)“優(yōu)化版”的時(shí)候,不幸的是,一天之后程序崩潰了!怎么回事?我檢查了所有用到Get函數(shù)的地方:
    boost::shared_ptr pClient = fdMap.Get(fd);
    這樣的代碼實(shí)在看不出有什么問題,而檢查Get函數(shù)的實(shí)現(xiàn),也沒有發(fā)現(xiàn)任何問題!
    事實(shí)上,問題是這樣的:
    仔細(xì)分析Get函數(shù)的操作,可以發(fā)現(xiàn),在“return vec_[fd];”之后,F(xiàn)dMap內(nèi)部的鎖已經(jīng)解開了,而此時(shí)調(diào)用線程得到的還是FdMap::vec_[fd]對(duì)象的引用,于是接下來給pClient賦值的操作就是一個(gè)沒有任何保護(hù)的過程。如果在把這個(gè)引用賦值給pClient的過程中,F(xiàn)dMap::vec_[fd]沒有被任何其他線程更改,那么一切正常;否則,程序就可能崩潰!
    明白了這個(gè)例子,相信大家在以后的優(yōu)化過程中,會(huì)更謹(jǐn)慎的處理多線程下的代碼。