muduo網路庫學習之Exception類、Thread 類封裝中的知識點(重點講pthread_atfork())

s1mba發表於2013-10-29

一、Exception類封裝

class Exception : public std::exception

 #include <execinfo.h>
 int backtrace(void **buffer, int size); // backtrace 棧回溯,儲存各個棧幀的地址
 char **backtrace_symbols(void *const *buffer, int size); // backtrace_symbols 根據地址,轉成相應的函式符號
// buffer 是指向一個陣列的指標,陣列存放的每一項是指向字串的指標
// backtrace_symbols 內部呼叫malloc 分配陣列空間,需要呼叫者自己釋放,但陣列指標指向的字串不需要呼叫者去釋放。


二、Thread類封裝

class Thread : boost::noncopyable

typedef boost::function<void ()> ThreadFunc;

具體實現分析見這裡。注意:結合CurrentThead.h 一起看,因為CurrentThead名稱空間內的一些函式是在
Thead.cc 裡面實現的,Thead類的一些成員函式也呼叫了CurrentThread名稱空間的一些函式。

1、獲取識別符號
pid --> getpid() //程式id
pthread_t --> pthread_self() //執行緒id
tid--> gettid() //執行緒真實id

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

(2)、有時候我們可能需要知道執行緒的真實pid。比如程式P1要向另外一個程式P2中的某個執行緒傳送訊號時,既不能使用P2的pid,更不能使用執行緒的pthread id,而只能使用該執行緒的真實pid,稱為tid。

(3)、有一個函式gettid()可以得到tid,但glibc並沒有實現該函式,只能通過Linux的系統呼叫syscall來獲取。
return syscall(SYS_gettid)

2、__thread,gcc內建的執行緒區域性儲存設施(每個執行緒有一份)
__thread只能修飾POD型別
POD型別(plain old data),與C相容的原始資料,例如,結構體和整型等C語言中的型別是 POD 型別,但帶有使用者定義的建構函式或虛擬函式的類則不是
__thread string   t_obj1(“simba”);     // 錯誤,不能呼叫物件的建構函式
__thread string* t_obj2 = new string;     // 錯誤,初始化只能是編譯期常量
__thread string* t_obj3 = NULL;     // 正確
若不是POD資料型別,但也想作為執行緒區域性儲存,可以使用執行緒特定資料TSD,參見以前的文章

namespace CurrentThread
{
  __thread int t_cachedTid = 0; //執行緒真實pid(tid)的快取,是減少系統呼叫::syscall(SYS_gettid),提高獲取tid的效率。
  __thread char t_tidString[32]; // tid 的字串表示
  __thread const char* t_threadName = "unknown"; //執行緒名稱
  const bool sameType = boost::is_same<int, pid_t>::value;     //判斷型別是否相同
  BOOST_STATIC_ASSERT(sameType);
}


3、boost::is_same

const bool sameType = boost::is_same<int, pid_t>::value;


4、assert(n == 6); (void) n;

在release 版本下assert語句被忽略,那麼n是未使用的變數,由於編譯選項會把警告當作錯誤,導致編譯不通過。

5、pthread_atfork()

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

pthread_atfork()在fork()之前呼叫,當呼叫fork時,內部建立子程式前在父程式中會呼叫prepare,內部建立子程式成功後,父程式會呼叫parent ,子程式會呼叫child。


 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 
#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>

void prepare(void)
{
    printf("pid = %d prepare ...\n"static_cast<int>(getpid()));
}

void parent(void)
{
    printf("pid = %d parent ...\n"static_cast<int>(getpid()));
}

void child(void)
{
    printf("pid = %d child ...\n"static_cast<int>(getpid()));
}


int main(void)
{
    printf("pid = %d Entering main ...\n"static_cast<int>(getpid()));

    pthread_atfork(prepare, parent, child);

    fork();

    printf("pid = %d Exiting main ...\n"static_cast<int>(getpid()));

    return 0;
}

執行結果如下:
simba@ubuntu:~/Documents/build/debug/bin$ ./pthread_atfork_test 
pid = 4791 Entering main ...
pid = 4791 prepare ...
pid = 4791 parent ...
pid = 4791 Exiting main ...
simba@ubuntu:~/Documents/build/debug/bin$ pid = 4792 child ...
pid = 4792 Exiting main ...


simba@ubuntu:~/Documents/build/debug/bin$ 

因為父程式首先退出,故bash提到前臺,子程式輸出混雜在命令列。


在實際程式設計中,最好不要多執行緒多程式,兩者擇其一,比如在多執行緒程式中呼叫fork 容易出現死鎖,因為子程式複製父程式的時候包含狀態變化,如鎖的狀態如果被複制的時候是已經加鎖,那麼子程式想加鎖的時候就會死鎖,因為不是本程式程式加的鎖,解鎖就不從談起,一直處於等待中。看下面的例子:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

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


pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

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

    return NULL;
}

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

    return 0;
}

首先主執行緒先呼叫pthread_create()建立一個子執行緒執行doit(),doit()裡面先加鎖,睡眠2s; 主執行緒睡眠1s後呼叫fork(),子程式會複製父程式的記憶體映像,此時全域性變數mutex 處於加鎖的狀態,所以子程式自己的mutex也是加鎖的,此時子程式是獨立執行的,也去執行doit(),在裡面試圖加鎖,因為本來mutex已經加鎖,而且根本沒有人會來解鎖,所以子程式就會死鎖。

執行結果如下:
simba@ubuntu:~/Documents/build/debug/bin$ ./deadlock_test
pid = 4823 Entering main ...
pid = 4823 begin doit ...
pid = 4825 begin doit ...
pid = 4823 end doit ...
pid = 4823 Exiting main ...
simba@ubuntu:~/Documents/build/debug/bin$ ps aux | grep deadlock_test
simba     4825  0.0  0.0  11684   144 pts/0    S    06:09   0:00 ./deadlock_test
simba     4830  0.0  0.0   4392   848 pts/0    S+   06:09   0:00 grep --color=auto deadlock_test
simba@ubuntu:~/Documents/build/debug/bin$ 

可以看到Exiting main 只輸出了一次,子程式根本沒有從doit()出來,ps 一下可以發現pid=4825 的程式一直沒有退出,注意死鎖跟殭屍程式是不同的,殭屍程式是退出但佔據著資源還沒被清理,而死鎖是一直沒有退出程式。

此時可以用pthread_atfork() 來解決這個問題:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>


pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

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

    return NULL;
}

void prepare(void)
{
    pthread_mutex_unlock(&mutex);
}

void parent(void)
{
    pthread_mutex_lock(&mutex);
}

int main(void)
{
    pthread_atfork(prepare, parent, NULL);
    printf("pid = %d Entering main ...\n"static_cast<int>(getpid()));
    pthread_t tid;
    pthread_create(&tid, NULL, doit, NULL);
    struct timespec ts = {10};
    nanosleep(&ts, NULL);
    if (fork() == 0)
    {
        doit(NULL);
    }
    pthread_join(tid, NULL);
    printf("pid = %d Exiting main ...\n"static_cast<int>(getpid()));

    return 0;
}

同樣的流程,但在執行fork() 建立子程式之前,先執行prepare(), 將子執行緒加鎖的mutex 解鎖下,然後為了與doit() 配對,在建立子程式成功後,父程式呼叫parent() 再次加鎖,這時父程式的doit() 就可以接著解鎖執行下去。而對於子程式來說,由於在fork() 建立子程式之前,mutex已經被解鎖,故複製的狀態也是解鎖的,所以執行doit()就不會死鎖了。
執行結果如下:
simba@ubuntu:~/Documents/build/debug/bin$ ./deadlock_test2
pid = 4905 Entering main ...
pid = 4905 begin doit ...
pid = 4908 begin doit ...
pid = 4905 end doit ...
pid = 4905 Exiting main ...
simba@ubuntu:~/Documents/build/debug/bin$ pid = 4908 end doit ...
pid = 4908 Exiting main ...


simba@ubuntu:~/Documents/build/debug/bin$ 

可以看到子程式也正常退出了。

參考:
muduo manual.pdf
《linux 多執行緒伺服器程式設計:使用muduo c++網路庫》


相關文章