多線程下的返回值優化陷阱

“函數的返回值優化”是我們對程序的一個常見優化手段。只要可能,我們都應該返回對象的有效引用,而不是重新生成一個臨時對象。但是,也許這種想法在多線程裏需要更仔細的斟酌一下。

我從一個簡單例子講起:

template<T><?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

class FdMap

{

    std::vector<T> 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一個把int映射到T對象的類型,採用std::vector作爲底層容器。它所做的工作很簡單:在需要的時候擴充容器vec_;如果訪問超過了範圍,返回T的默認值。

如果FdMap要在多線程下工作,那麼我們需要加一點代碼,進行必要的同步:

template<T>

class FdMap

{

    std::vector<T> vec_;

    Mutex          mutex_;  //鎖同步對象

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是對應的範圍保衛類型,在構造的時候鎖住mutex_,析構的時候解鎖。到目前爲止,FdMap類非常完美,沒有任何問題。

在實際應用中,我們可能把FdMap用於關聯socket fd和對應的客戶端連接對象(例如ClientData),於是可能的實例化是FdMap<ClientData *>

不過有經驗的程序員馬上會看出,直接在FdMap裏存儲ClientData指針是不行的。如果某個線程Get到了一個fdClientData指針,而另一個線程卻試圖關閉這個fd連接,那麼ClientData對象是釋放掉,還是繼續有效?如果釋放了ClientData對象,那麼前一個線程隨後的訪問就會失效;如果繼續保留ClientData對象,那麼誰也不知道該在何時釋放它了,就會內存泄漏。

正確的做法是,在FdMap裏存儲ClientData的智能指針,那麼哪個線程都不需要特意釋放ClientData對象,智能指針會在適當的時候處理。爲了保險,我們決定使用boost::shared_ptr,因爲大家都認爲它是正確的。

OK,我們有了一個設計完美,運行正確的FdMap,完整的實例化是:

FdMap<boost::shared_ptr<ClientData> >

 

終於有一天,我們需要優化代碼,於是我們重新審視上面的代碼。Get函數!是的,它的返回值是否可以優化?因爲對於智能指針,即使是拷貝構造函數都是昂貴的,能夠減少一個臨時對象的構造,對於我們的確太誘人了。

但是有一個明顯的問題:當fd不在範圍內的時候,返回誰的引用?且看下面的實現。

下面是我們的“優化”:

    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;

    }

通過增加一個局部靜態常量DEF,解決了返回默認引用的問題。如果fd在範圍內,那麼返回vec_裏的對象引用,是沒有問題的。

但是當我再次運行這個“優化版”的時候,不幸的是,一天之後程序崩潰了!怎麼回事?我檢查了所有用到Get函數的地方:

boost::shared_ptr<ClientData> pClient = fdMap.Get(fd);

這樣的代碼實在看不出有什麼問題,而檢查Get函數的實現,也沒有發現任何問題!

 

事實上,問題是這樣的:

仔細分析Get函數的操作,可以發現,在“return vec_[fd];”之後,FdMap內部的鎖已經解開了,而此時調用線程得到的還是FdMap::vec_[fd]對象的引用,於是接下來給pClient賦值的操作就是一個沒有任何保護的過程。如果在把這個引用賦值給pClient的過程中,FdMap::vec_[fd]沒有被任何其他線程更改,那麼一切正常;否則,程序就可能崩潰!

明白了這個例子,相信大家在以後的優化過程中,會更謹慎的處理多線程下的代碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章