1. 程式人生 > >C++中 執行緒函式為靜態函式 及 類成員函式作為回撥函式

C++中 執行緒函式為靜態函式 及 類成員函式作為回撥函式

 執行緒函式為靜態函式:

  執行緒控制函式和是不是靜態函式沒關係,靜態函式是在構造中分配的地址空間,只有在析構時才釋放也就是全域性的東西,不管執行緒是否執行,靜態函式的地址是不變的,並不在執行緒堆疊中static只是起了一個裝飾的作用,所以二者並沒有必然的關係

  執行緒也是一種資源,是受系統排程的。因此,你必須要讓系統知道你的執行緒的起始位置,即執行緒函式的指標。Window系統的介面(API)是C標準的,系統在啟動執行緒時,需要直接得到執行緒的起始位置,因此你也必須向系統直接傳遞這樣一個直接的函式指標,這與C++其它的特性是無關的。

  因為對於非靜態成員函式而言,預設情況下,引數列表中都會有一個this指標,例如fun(自定義引數),實際上編譯後就變成這樣型別:fun(自定義引數,某個類 * this)。這樣編譯就會出錯,多了一個引數,所以這個函式就不能作為執行緒函數了。加上static修飾之後,類的成員函式就不會加上預設this了,所以符合呼叫規定。

  在C++的類中,普通成員函式不能作為pthread_create的執行緒函式,如果要作為pthread_create中的執行緒函式,必須是static !在C語言中,我們使用pthread_create建立執行緒,執行緒函式是一個全域性函式,所以在C++中,建立執行緒時,也應該使用一個全域性函式。static定義的類的成員函式就是一個全域性函式。

 

類成員函式作為回撥函式

 

  回撥函式是基於C程式設計的Windows SDK的技術,不是針對C++的,程式設計師可以將一個C函式直接作為回撥函式,但是如果試圖直接使用C++的成員函式作為回撥函式將發生錯誤,甚至編譯就不能通過。 

 

  普通的C++成員函式都隱含了一個傳遞函式作為引數,亦即“this”指標,C++通過傳遞一個指向自身的指標給其成員函式從而實現程式函式可以訪問C++的資料成員。這也可以理解為什麼C++類的多個例項可以共享成員函式但是確有不同的資料成員。由於this指標的作用,使得將一個CALLBACK型的成員函式作為回撥函式安裝時就會因為隱含的this指標使得函式引數個數不匹配,從而導致回撥函式安裝失敗。

 

  這樣從理論上講,C++類的成員函式是不能當作回撥函式的。但我們在用C++程式設計時總希望在類內實現其功能,即要保持封裝性,如果把回撥函式寫作普通函式有諸多不便。經過網上搜索和自己研究,發現了幾種巧妙的方法,可以使得類成員函式當作回撥函式使用。

  這裡採用Linux C++中執行緒建立函式pthread_create舉例,其原型如下:

   int pthread_create( pthread_t *restrict tidp , const pthread_attr_t *restrict attr , void* (*start_rtn)(void*) , void *restrict arg );  

  第一個引數為指向執行緒識別符號的指標。

  第二個引數用來設定執行緒屬性。

  第三個引數是執行緒執行函式的起始地址,即回撥函式。

  最後一個引數是執行函式的引數。

 

  這裡我們只關注第三個引數start_run,它是一個函式指標,指向一個以void*為引數,返回值為void*的函式,這個函式被當作執行緒的回撥函式使用,執行緒啟動後便會執行該函式的程式碼。

 方法一:回撥函式為普通函式,但在函式體內執行成員函式

 

複製程式碼

class MyClass  
{  
    pthread_t TID;  
public:  
    void func()  
    {  
        //子執行緒執行程式碼  
    }  
  
    bool startThread()  
    {//啟動子執行緒  
        int ret = pthread_create( &TID , NULL , callback , this );  
        if( ret != 0 )  
            return false;  
        else  
            return true;  
    }  
};  
  
static void* callback( void* arg )  
{//回撥函式  
    ((MyClass*)arg)->func();呼叫成員函式  
    return NULL;  
}  
  
int main()  
{  
    MyClass a;  
    a.startThread();  
}  

複製程式碼

 

  類MyClass需要在自己內部開闢一個子執行緒來執行成員函式func()中的程式碼,子執行緒通過呼叫startThread()成員函式來啟動。這裡將回調函式callback寫在了類外面,傳遞的引數是一個指向MyClass物件的指標(在pthrad_create()中由第4個引數this指定),回撥函式經過強制轉換把void*變為MyClass*,然後再呼叫arg->func()執行子執行緒的程式碼。

  這樣做的原理是把當前物件的指標當作引數先交給一個外部函式,再由外部函式呼叫類成員函式,以外部函式作為回撥函式,但執行的是成員函式的功能,這樣相當於在中間作了一層轉換。缺點是回撥函式在類外,影響了封裝性,這裡把callback()限定為static,防止在其它檔案中呼叫此函式。

方法二:回撥函式為類內靜態成員函式,在其內部呼叫成員函式

在方法一上稍作更改,把回撥函式搬到類MyClass裡,這樣就保持了封裝性。程式碼如下:

 

複製程式碼

class MyClass  
{  
    static MyClass* CurMy;//儲存回撥函式呼叫的物件  
    static void* callback(void*);//回撥函式  
    pthread_t TID;  
    void func()  
    {  
        //子執行緒執行程式碼  
    }  
      
    void setCurMy()  
    {//設定當前物件為回撥函式呼叫的物件  
        CurMy = this;  
    }  
public:  
    bool startThread()  
    {//啟動子執行緒  
        setCurMy();  
        int ret = pthread_create( &TID , NULL , MyClass::callback , NULL );  
        if( ret != 0 )  
            return false;  
        else  
            return true;  
    }  
};  
MyClass* MyClass::CurMy = NULL;  
void* MyClass::callback(void*)  
{  
    CurMy->func();  
    return NULL;  
}  
  
int main()  
{  
    MyClass a;  
    a.startThread();  
}  

複製程式碼

 

  類MyClass有了1個靜態資料成員CurMy和1個靜態成員函式callback。CurMy用來儲存一個物件的指標,充當方法一中回撥函式的引數arg。callback當作回撥函式,執行CurMy->func()的程式碼。每次建立執行緒前先要呼叫setCurMy()來讓CurMy指向當前自己。

  這個方法的好處時封裝性得到了很好的保護,MyClass對外只公開一個介面startThread(),子執行緒程式碼和回撥函式都被設為私有,外界不可見。另外沒有佔用callback的引數,可以從外界傳遞引數進來。但每個物件啟動子執行緒前一定要注意先呼叫setCurMy()讓CurMy正確的指向自身,否則將為其它物件開啟執行緒,這樣很引發很嚴重的後果。

方法三:對成員函式進行強制轉換,當作回撥函式

 

複製程式碼

class MyClass  
{  
    pthread_t TID;  
    void func()  
    {  
        //子執行緒執行程式碼  
    }  
public:  
    bool startThread()  
    {//啟動子執行緒  
        typedef void* (*FUNC)(void*);//定義FUNC型別是一個指向函式的指標,該函式引數為void*,返回值為void*  
        FUNC callback = (FUNC)&MyClass::func;//強制轉換func()的型別  
        int ret = pthread_create( &TID , NULL , callback , this );  
        if( ret != 0 )  
            return false;  
        else  
            return true;  
    }  
};  
  
int main()  
{  
    MyClass a;  
    a.startThread();  
}  

複製程式碼

 

  這個方法是原理是,MyClass::func最終會轉化成 void func(MyClass *this); 也就是說在原第一個引數前插入指向物件本身的this指標。可以利用這個特性寫一個非靜態類成員方法來直接作為執行緒回撥函式。對編譯器而言,void (MyClass::*FUNC1)()和void* (*FUNC)(void*)這兩種函式指標雖然看上去很不一樣,但他們的最終形式是相同的,因此就可以把成員函式指標強制轉換成普通函式的指標來當作回撥函式。在建立執行緒時要把當前物件的指標this當作引數傳給回撥函式(成員函式func),這樣才能知道執行緒是針對哪個物件建立的。

  方法三的封裝性比方法二更好,因為不涉及多個物件共用一個靜態成員的問題,每個物件可以獨立地啟動自己的執行緒而不影響其它物件。