muduo網路庫學習筆記(3):Thread類

li27z發表於2016-08-08

muduo網路庫採用了基於物件的程式設計思想來封裝執行緒類。

類圖如下:
這裡寫圖片描述
變數numCreated_表示建立的執行緒個數,型別為AtomicInt32,用到了我們上篇所說的原子性操作。
Thread類中還用到了CurrentThread類。

程式碼要點如下:

(1)執行緒識別符號
Linux中,每個程式有一個pid,型別為pid_t,由getpid()取得。Linux下的POSIX執行緒也有一個id,型別為pthread_t,由pthread_self()取得,該id由執行緒庫維護,其id空間是各個程式獨立的(即不同程式中的執行緒可能有相同的id)。Linux中的POSIX執行緒庫實現的執行緒其實也是一個程式(LWP:輕量級程式),只是該程式與主程式(啟動執行緒的程式)共享一些資源而已,比如程式碼段,資料段等。

有時候我們可能需要知道執行緒的真實pid。比如程式P1要向另外一個程式P2中的某個執行緒傳送訊號時,既不能使用P2的pid,更不能使用執行緒的pthread id,而只能使用該執行緒的真實pid,稱為tid
函式gettid()可以得到tid,但glibc並沒有實現該函式,只能通過Linux的系統呼叫syscall來獲取。

return syscall(SYS_gettid)

因為使用系統呼叫開銷很大,所以我們需要對所獲取的tid做一個快取,防止每次都使用系統呼叫,從而提高獲取tid的效率。

程式碼片段:快取tid
檔名:CurrentThread.h

......

extern __thread int t_cachedTid;  // 執行緒真實pid(tid)的快取

......

inline int tid()
{
    if (t_cachedTid == 0)
    {
      cacheTid();
    }
    return t_cachedTid;
}
程式碼片段:cacheTid()
檔名:Thread.cc

void CurrentThread::cacheTid()
{
  if (t_cachedTid == 0)
  {
    t_cachedTid = detail::gettid();
    int n = snprintf(t_tidString, sizeof t_tidString, "%5d ", t_cachedTid);

    // (void) n; 的用法是為了防止未使用變數n而出現編譯錯誤
    assert(n == 6); (void) n;
  }
}

(2)__thread關鍵字和POD型別
__thread是GCC內建的執行緒區域性儲存設施,存取效率可以和全域性變數相比。__thread變數在每一個執行緒有一份獨立實體,各個執行緒的值互不干擾。可以用來修飾那些帶有全域性性且值可能變,但是又不值得用全域性變數保護的變數。用一個例子來理解它的用法。

#include <pthread.h>
#include <iostream>
#include <unistd.h>

using namespace std;

//__thread int var = 5;
int var = 5;

void *worker1(void* arg);
void *worker2(void* arg);

int main()
{
        pthread_t p1, p2;

        pthread_create(&p1, NULL, worker1, NULL);
        pthread_create(&p2, NULL, worker2, NULL);
        pthread_join(p1, NULL);
        pthread_join(p2, NULL);

        return 0;
}

void *worker1(void* arg)
{
        cout << ++var << endl;
}

void *worker2(void* arg)
{
        cout << ++var << endl;
}

/**
 * 使用__thread關鍵字,輸出為: 
 *                         6
 *                         6
 * 
 * 不使用__thread關鍵字,輸出為:
 *                         6   
 *                         7
 * /

**注:**__thread只能修飾POD型別,不能修飾class型別,因為無法自動呼叫建構函式和解構函式。

POD型別(plain old data)是指與C相容的原始資料型別,例如,結構體和整型等C語言中的型別就是 POD 型別,但帶有使用者定義的建構函式或虛擬函式的類則不是:

__thread可以用於修飾全域性變數、函式內的靜態變數,但是不能用於修飾函式的區域性變數或者class的普通成員變數。

另外,__thread變數的初始化只能用編譯器常量。

__thread string t_obj1(“hello”);    // 錯誤,不能呼叫物件的建構函式
__thread string* t_obj2 = new string;   // 錯誤,初始化必須用編譯期常量
__thread string* t_obj3 = NULL; // 正確,但是需要手工初始化並銷燬物件

(3)pthread_atfork()函式

#include <pthread.h>
int pthread_atfork(void (*prepare)(void), 
                    void (*parent)(void), 
                    void (*child)(void));

用法:呼叫fork時,內部建立子程式前在父程式中會呼叫prepare,內部建立子程式成功後,父程式會呼叫parent ,子程式會呼叫child。

(4)多執行緒與fork()
對於編寫多執行緒程式來說,最好不要再呼叫fork(),即不要編寫多執行緒多程式程式。因為Linux的fork()只克隆當前執行緒的thread of control ,不克隆其他執行緒。fork()之後,除了當前執行緒之外,其他執行緒都消失了,也就是說,不能一下子fork()出一個和父程式一樣的多執行緒子程式。

fork()之後子程式中只有一個執行緒,其他執行緒都消失了,這就造成一個危險的局面。其他執行緒可能正好位於臨界區之內,持有了某個鎖,而它突然死亡,再也沒有機會去解鎖了。如果子程式試圖再對同一個mutex加鎖,就會立刻死鎖。

一個在多執行緒程式裡fork造成死鎖的例子:

/*
 死鎖的原因:
 1. 執行緒裡的doit()先執行
 2. doit執行的時候會給互斥量mutex加鎖
 3. mutex的內容會原樣拷貝到fork出來的子程式中(在此之前,mutex變數的內容已經被執行緒改寫成鎖定狀態)
 4. 子程式再次呼叫doit的時候,在給互斥量mutex加鎖的時候會發現它已經被加鎖,所以就一直等待,直到擁有該互斥體的程式釋放它(實際上沒有人擁有這個mutex鎖)
 5. 執行緒的doit執行完成之前會把自己的mutex釋放,但這時的mutex和子程式裡的mutex已經是兩份記憶體.所以即使釋放了mutex鎖也不會對子程式裡的mutex造成什麼影響
*/

#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* doit(void* arg)
{
    printf("%d begin doit\n",static_cast<int>(getpid()));
    pthread_mutex_lock(&mutex);
    struct timespec ts = {2, 0};
    nanosleep(&ts, NULL);
    pthread_mutex_unlock(&mutex);
    printf("%d end doit\n",static_cast<int>(getpid()));

    return NULL;
}

int main(void)
{
    printf("%d enter main\n", static_cast<int>(getpid()));
    pthread_t tid;
    pthread_create(&tid, NULL, doit, NULL);
    struct timespec ts = {1, 0};
    nanosleep(&ts, NULL);
    if (fork() == 0)
    {
        doit(NULL);
    }
    pthread_join(tid, NULL);
    printf("%d exit main\n",static_cast<int>(getpid()));

    return 0;
}

執行結果如下:
這裡寫圖片描述

相關文章