我是一個線程(節選)

又是一個陽光明媚的週末,給廣大學生朋友和初學者寫點基礎教程吧。以下是正文,請您鑑賞。

很多年以前,技術面試的時候面試官經常會問“程序什麼時候需要開啓新的線程”這樣的問題,那個時候多核CPU纔剛開始普及,很多人也是開始逐漸接觸多線程技術。而如今多核CPU和多線程編程技術已經是下里巴人的技術了,所以本文不會花大氣力再去回答“程序什麼時候需要開啓新的線程”,簡單地解釋一下,就是爲了提高解決問題的效率,畢竟大多數情況下,多個cpu分工做一件事總比單個CPU做快許多吧。(主要是現在的面試官也很少再問“程序什麼時候需要開啓新的線程”這樣的問題了,哈哈。)

多線程編程在現代軟件開發中是如此的重要,以至於熟練使用多線程編程是一名合格的後臺開發人員的基本功,注意,我這裏用的是基本功一詞。它是如此的重要,所以您應該掌握它。本文將介紹多線程的方方面面,從基礎的知識到高級進階。讓我們開始吧。

線程的基礎知識

線程的英文單詞是thread,翻譯成對應的中文有”分支“、”枝幹“的意思,當然這裏翻譯成”線程“屬於意譯了。提高線程就不得不提與線程相關聯的另外一個概念”進程“,一個”進程“代表中計算機中實際跑起來的一個程序,在現代操作系統的保護模式下,每個進程擁有自己獨立的進程地址空間和上下文堆棧。但是就一個程序本身執行的操作來說,進程其實什麼也不做(不執行任何進程代碼),它只是提供一個大環境容器,在進程中實際的執行體是”線程“。wiki百科上給線程的定義是:

In computer science, a thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler, which is typically a part of the operating system. 計算機科學中,線程是操作系統管理的、可以執行編制好的最小單位的指令序列的調度器。

翻譯的有點拗口,通俗地來說,進程是進程中實際執行代碼的最小單元,它由操作系統安排調度(何時啓動、何時運行和暫停以及何時消亡)。

進程與線程的區別與關係這裏就不再多說了,任何一本關於操作系統的書籍都會有詳細的介紹。這裏需要重點強調的是如下幾個問題,這也是我們在實際開發中使用多線程需要搞明白的問題。

一個進程至少有一個線程

上文也說了,線程是進程中實際幹活的單位,因此一個進程至少得有一個線程,我們把這個線程稱之爲”主線程“,也就是說,一個進程至少要有一個主線程

主線程退出,支線程也將退出

當一個進程存在多個線程時,如果主線程執行結束了,那麼這個時候即使支線程(也可以叫工作線程)還沒完成相關的代碼執行,支線程也會退出,也就是說,主線程一旦退出整個進程也就結束了。之所以強調這一點是,很多多線程編程的初學者經常犯在工作線程寫了很多邏輯代碼,但是沒有注意到主線程已經提前退出,導致這些工作線程的代碼來不及執行。解決這一問題的方案很多,核心就是讓主線程不要退出,或者至少在工作線程完成工作之前主線程不要退出。常見的解決方案有主線程啓動一個循環或者主線程等待工作線程退出後再退出(下文將會詳細介紹)。

某個線程崩潰,會導致進程退出嗎?

這是一個常見的面試題,還有一種問法是:進程中某個線程崩潰,是否會對其他線程造成影響?

一般來說,每個線程都是獨立執行的單位,每個線程都有自己的上下文堆棧,一個線程的的崩潰不會對其他線程造成影響。但是通常情況下,一個線程崩潰會產生一個進程內的錯誤,例如在linux操作系統中,可能會產生一個segment fault錯誤,這個錯誤會產生一個信號,操作系統默認對這個信號的處理就是關閉進程,整個進程都被銷燬了,這樣的話這個進程中存在的其他線程自然也就不存在了。

線程基本操作

線程的創建

在使用線程之前,我們首先要學會如何創建一個新的線程。不管是哪個庫還是哪種高級語言(如Java),線程的創建最終還是調用操作系統的API來進行的。我們這裏先介紹操作系統的接口,這裏分linux和Windows兩個常用的操作系統平臺來介紹。當然,這裏並不是照本宣科地把linux man手冊或者msdn上的函數簽名搬過來,這裏只介紹我們實際開發中常用的參數和需要注意的重難點。

linux線程創建

linux平臺上使用pthread_create這個API來創建線程,其函數簽名如下:

int pthread_create(pthread_t *thread, 
                   const pthread_attr_t *attr,
                   void *(*start_routine) (void *),
                   void *arg);
  • 參數thread,是一個輸入參數,如果線程創建成功,通過這個參數可以得到創建成功的線程ID(下文會介紹線程ID的知識)。
  • 參數attr指定了該線程的屬性,一般設置爲NULL,表示使用默認屬性。
  • 參數start_routine指定了線程函數,這裏需要注意的是這個函數的調用方式必須是__cedel調用,由於在C/C++中定義函數時默認的調用方式就是__cedel調用,所以一般很少有人注意到這一點。而後面我們介紹在Windows操作系統上使用CreateThread定義線程函數時必須使用__stdcall調用方式時,我們就必須顯式申明函數的調用方式了。

也就是說,如下函數的調用方式是等價的:

1//代碼片段1: 不顯式指定函數調用方式,其調用方式爲默認的__cdecl
2void start_routine (void* args)
3  {
4}
5
6//代碼片段2: 顯式指定函數調用方式爲默認的__cdecl,等價於代碼片段1
7void __cdecl start_routine (void* args)
8  {
9}
  • 參數arg,通過這一參數可以在創建線程時將某個參數傳入線程函數中,由於這是一個void*類型,我們可以方便我們最大化地傳入任意多的信息給線程函數。(下文會介紹一個使用示例)
  • 返回值:如果成功創建線程,返回0;如果創建失敗,則返回響應的錯誤碼,常見的錯誤碼有EAGAINEINVALEPERM

下面是一個使用pthread_create創建線程的簡單示例:

 1#include <pthread.h>
 2#include <unistd.h>
 3
 4void threadfunc(void* arg)
 5  {
 6  while(1)
 7  {
 8    //睡眠1秒
 9    sleep(1);
10
11    printf("I am New Thread!\n");
12  }
13}
14
15int main()
16  {
17  pthread_t threadid;
18  pthread_create(&threadid, NULL, threadfunc, NULL);
19
20  while (1)
21  {
22    sleep(1);
23    //權宜之計,讓主線程不要提前退出
24  }
25
26  return 0;
27}
Windows線程創建

Windows上創建線程使用CreateThread,其函數簽名如下:

1HANDLE CreateThread(
2  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
3  SIZE_T                  dwStackSize,
4  LPTHREAD_START_ROUTINE  lpStartAddress,
5  LPVOID                  lpParameter,
6  DWORD                   dwCreationFlags,
7  LPDWORD                 lpThreadId
8);
  • 參數lpThreadAttributes,是線程的安全屬性,一般設置爲NULL。
  • 參數dwStackSize,線程的棧空間大小,單位爲字節數,一般指定爲0,表示使用默認大小。
  • 參數lpStartAddress,爲線程函數,其類型是LPTHREAD_START_ROUTINE,這是一個函數指針類型,其定義如下: typedef DWORD ( __stdcall *LPTHREAD_START_ROUTINE )(LPVOID lpThreadParameter); 需要注意的是,Windows上創建的線程的線程函數其調用方式必須是__stdcall,如果您將如下函數設置成線程函數是不行的: DWORD threadfunc(LPVOID lpThreadParameter); 如上文所說,如果您不指定函數的調用方式,默認使用默認調用方式__cdecl,而這裏的線程函數要求是__stdcall,因此你必須在函數名前面顯式指定函數調用方式爲__stdcall。 DWORD __stdcall threadfunc(LPVOID lpThreadParameter); Windows上的宏WINAPICALLBACK這兩個宏的定義都是__stdcall。因爲您在項目中看到的線程函數的簽名大多寫成如下兩種形式的一種: 1//寫法1 2DWORD WINAPI threadfunc(LPVOID lpThreadParameter); 3//寫法2 4DWORD CALLBACK threadfunc(LPVOID lpThreadParameter);
  • 參數lpParameter爲傳給線程函數的參數,和linux下的pthread_create函數的arg一樣,這實際上也是一個void類型(LPVOID類型是用過typedef包裝後的void類型)。
  • 參數wCreationFlags,是一個32位無符號整型(DWORD),一般設置爲0,表示創建好線程後立即啓動線程的運行;有一些特殊的情況,我們不希望創建線程後立即開始執行,可以將這個值設置爲4(對應Windows定義的宏CREATE_SUSPENDED),後面在需要的時候,再使用ResumeThread這個API讓線程運行起來。
  • 參數lpThreadId,爲線程創建成功返回的線程ID,這也是一個32位無符號整數(DWORD)。
  • 返回值:Windows上使用句柄(HANDLE類型)來管理線程對象,句柄本質上是內核句柄表中的索引值。如果成功創建線程,返回該線程的句柄;如果創建失敗,返回NULL。

下面的代碼片段,演示了Windows上如何創建一個線程:

 1#include <Windows.h>
 2#include <stdio.h>
 3
 4DWORD WINAPI ThreadProc(LPVOID lpParameters)
 5  {
 6    while (true)
 7    {
 8        //睡眠1秒,Windows上的Sleep函數參數事件單位爲毫秒
 9        Sleep(1000);
10
11        printf("I am New Thread!\n");
12    }
13}
14
15int main()
16  {
17    DWORD dwThreadID;
18    HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadID);
19    if (hThread == NULL)
20    {
21        printf("Failed to CreateThread.\n");
22    }
23
24    while (true)
25    {
26        Sleep(1000);
27        //權宜之計,讓主線程不要提前退出
28    }
29
30    return 0;
31}
CRT線程創建

這裏的CRT,指的是C Runtime(C運行時),通俗地說就是C函數庫。C庫也提供了一套用於創建線程的函數(當然這個函數底層還是調用相應的操作系統平臺的線程創建API),這裏之所以提到這點是因爲,由於C庫函數是同時被Linux和Windows等操作系統支持的,所以使用C庫函數創建線程可以直接寫出跨平臺的代碼。由於其跨平臺性,實際項目開發中推薦使用這個函數來創建線程。

C庫創建線程常用的函數是_beginthreadex,申明位於process.h頭文件中,其簽名如下:

1uintptr_t _beginthreadex( 
2   void *security,  
3   unsigned stack_size,  
4   unsigned ( __stdcall *start_address )( void * ),  
5   void *arglist,  
6   unsigned initflag,  
7   unsigned *thrdaddr   
8);  

函數簽名基本上和Windows上的CreateThread函數基本一致,這裏就不再贅述了。

以下是使用_beginthreadex創建線程的一個例子:

 1#include <process.h>
 2//#include <Windows.h>
 3#include <stdio.h>
 4
 5unsigned int __stdcall threadfun(void* args)
 6  {
 7    while (true)
 8    {        
 9        //Sleep(1000);
10
11        printf("I am New Thread!\n");
12    }
13}
14
15int main(int argc, char* argv[])
16  {
17    unsigned int threadid;
18    _beginthreadex(0, 0, threadfun, 0, 0, &threadid);
19
20    while (true)
21    {
22        //Sleep(1000);
23        //權宜之計,讓主線程不要提前退出
24    }
25
26    return 0;
27}
C++11提供的std::thread類

無論是linux還是Windows上創建線程的API,都有一個非常不方便的地方,就是線程函數的簽名必須是固定的格式(參數個數和類型、返回值類型都有要求)。新的C++11標準引起了一個新的類std::thread(需要包含頭文件<thread>),使用這個類的可以將任何簽名形式的函數作爲線程函數。以下代碼分別創建兩個線程,線程函數簽名不一樣:

 1#include <stdio.h>
 2#include <thread>
 3
 4void threadproc1()
 5  {
 6    while (true)
 7    {
 8        printf("I am New Thread 1!\n");
 9    }
10}
11
12void threadproc2(int a, int b)
13  {
14    while (true)
15    {
16        printf("I am New Thread 2!\n");
17    }
18}
19
20int main()
21  {
22    //創建線程t1
23    std::thread t1(threadproc1);
24    //創建線程t2
25    std::thread t2(threadproc2, 1, 2);
26
27    while (true)
28    {
29        //Sleep(1000);
30        //權宜之計,讓主線程不要提前退出
31    }
32
33    return 0;
34}

當然,初學者在使用std::thread時容易犯如下錯誤:即在std::thread對象在線程運行期間必須是有效的。看下面的代碼:

 1#include <stdio.h>
 2#include <thread>
 3
 4void threadproc()
 5  {
 6    while (true)
 7    {
 8        printf("I am New Thread!\n");
 9    }
10}
11
12void func()
13  {
14    std::thread t(threadproc);
15}
16
17int main()
18  {
19    func();
20
21    while (true)
22    {
23        //Sleep(1000);
24        //權宜之計,讓主線程不要提前退出
25    }
26
27    return 0;
28}

上述代碼在func中創建了一個線程,然後又在main函數中調用func方法,乍一看好像代碼沒什麼問題,但是在實際運行時程序會崩潰。崩潰的原因是,當func函數調用結束後,func中局部變量t線程對象)就會被銷燬了,而此時線程函數仍然在運行。這就是我所說的,使用std::thread類時,必須保證線程運行期間,其線程對象有效。這是一個很容易犯的錯誤,解決這個問題的方法是,std::thread對象提供了一個detach方法,這個方法讓線程對象線程函數脫離關係,這樣即使線程對象被銷燬,仍然不影響線程函數的運行。我們只需要在在func函數中調用detach方法即可,代碼如下:

1//其他代碼保持不變,這裏就不重複貼出來了
2void func()
3  {
4    std::thread t(threadproc);
5    t.detach();
6}

然而,更多的時候,我們需要線程對象去控制和管理線程的運行和生命週期,我們的代碼應該儘量保證線程對象在線程運行期間有效,而不是單純地調用detach方法。

線程ID

一個線程創建成功以後,我們可以拿到一個線程ID,線程ID是在整個操作系統範圍內是唯一的。我們可以使用線程ID來標識和區分線程,例如我們在日誌文件中,我們把打印日誌的所在的線程ID也一起打印出來,這樣也方便我們判斷和排查問題。創建線程時,上文也介紹了可以通過pthread_create函數的第一個參數thread(linux平臺)和CreateThread函數的最後一個參數lpThreadId(Windows平臺)得到線程的ID。大多數時候,我們需要在當前調用線程中獲取當前線程的ID,在linux平臺上可以使用pthread_self函數,在Windows平臺上可以使用GetCurrentThreadID函數獲取,這兩個函數的簽名分別如下:

pthread_t pthread_self(void);

DWORD GetCurrentThreadId();

這兩個函數比較簡單,這裏就不介紹了,無論是pthread_t還是DWORD類型,都是一個32位無符號整型值。

Windows操作系統中可以在任務管理器中查看某個進程的線程數量:

pstack命令

linux系統中可以通過pstack命令查看一個進程的線程數量和每個線程的調用堆棧情況。

pstack pid

pid設置爲要查看的進程的ID即可。以我機器上nginx的worker進程爲例,首先使用ps命令查看下nginx進程ID,然後使用pstack即可查看該進程每個線程的調用堆棧(我這裏的nginx只有一個線程,如果有多個線程,會顯示每個線程的調用堆棧):

 1[root@iZ238vnojlyZ ~]# ps -ef | grep nginx
 2root      2150     1  0 May22 ?        00:00:00 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
 3nginx     2151  2150  0 May22 ?        00:00:07 nginx: worker process
 4root     16621 16541  0 18:53 pts/0    00:00:00 grep --color=auto nginx
 5[root@iZ238vnojlyZ ~]# pstack 2151
 6#0  0x00007f70a61ca2a3 in __epoll_wait_nocancel () from /lib64/libc.so.6
 7#1  0x0000000000437313 in ngx_epoll_process_events ()
 8#2  0x000000000042efc3 in ngx_process_events_and_timers ()
 9#3  0x0000000000435681 in ngx_worker_process_cycle ()
10#4  0x0000000000434104 in ngx_spawn_process ()
11#5  0x0000000000435854 in ngx_start_worker_processes ()
12#6  0x000000000043631b in ngx_master_process_cycle ()
13#7  0x0000000000412229 in main ()
C++11的獲取當前線程ID的方法

C++11的線程庫可以使用std::this_thread類的get_id獲取當前線程的ID,這是一個靜態類方法。

當然也可以使用std::threadget_id獲取指定線程的ID,這是一個實例方法。

但是get_id方法返回的是一個包裝類型的std::thread::id對象,不可以直接強轉成整型,也沒有提供任何轉換成整型的接口。所以,我們一般使用std::cout這樣的輸出流來輸出,或者先轉換爲std::ostringstream對象,再轉換成字符串類型,然後把字符串類型轉換成我們需要的整型。這一點,算是C++11的線程庫不是很方便的地方。

 1#include <thread>
 2#include <iostream>
 3#include <sstream>
 4
 5void worker_thread_func()
 6  {
 7    while (true)
 8    {
 9
10    }
11}
12
13int main()
14  {
15    std::thread t(worker_thread_func);
16    //獲取線程t的ID
17    std::thread::id worker_thread_id = t.get_id();
18    std::cout << "worker thread id: " << worker_thread_id << std::endl;
19
20    //獲取主線程的線程ID
21    std::thread::id main_thread_id = std::this_thread::get_id();
22    //先將std::thread::id轉換成std::ostringstream對象
23    std::ostringstream oss;
24    oss << main_thread_id;
25    //再將std::ostringstream對象轉換成std::string
26    std::string str = oss.str();
27    //最後將std::string轉換成整型值
28    int threadid = atol(str.c_str());
29
30    std::cout << "main thread id: " << threadid << std::endl;
31
32    while (true)
33    {
34        //權宜之計,讓主線程不要提前退出
35    }
36
37    return 0;
38}

程序運行結果如下:

等待線程結束

實際項目開發中,我們常常會有這樣一種需求,即一個線程需要等待另外一個線程執行完任務退出後再繼續執行。這在linux和Windows操作系統中都提供了相應的操作系統API,我們來分別介紹一下。

linux下等待線程結束

linux下使用pthread_join函數等待某個線程結束,其函數簽名如下:

int pthread_join(pthread_t thread, void **retval);
  • 參數thread,需要等待的線程id。
  • 參數retval,輸出參數,用於接收等待退出的線程的退出碼(Exit Code)。

pthread_join函數等待其他線程退出期間會掛起等待的線程,被掛起的線程不會消耗寶貴任何CPU時間片。直到目標線程退出後,等待的線程會被喚醒。

我們通過一個實例來演示一下這個函數的使用方法,實例功能如下:

程序啓動時,開啓一個工作線程,工作線程將當前系統時間寫入文件中後退出,主線程等待工作線程退出後,從文件中讀取出時間並顯示在屏幕上。

 1#include <stdio.h>
 2#include <pthread.h>
 3
 4#define TIME_FILENAME "time.txt"
 5
 6void fileThreadFunc(void* arg)
 7  {
 8    time_t now = time(NULL);
 9    struct tm* t = localtime(&now);
10    char timeStr[32] = {0};
11    snprintf(timeStr, 32, "%04d/%02d/%02d %02d:%02d:%02d", 
12             t->tm_year+1900,
13             t->tm_mon+1,
14             t->tm_mday,
15             t->tm_hour,
16             t->tm_min,
17             t->tm_sec);
18    //文件不存在,則創建;存在,則覆蓋。
19    FILE* fp = fopen(TIME_FILENAME, "w");
20    if (fp == NULL)
21    {
22      printf("Failed to create time.txt.\n");
23        return;
24    }
25
26    size_t sizeToWrite = strlen(timeStr) + 1;
27    size_t ret = fwrite(timeStr, 1, sizeToWrite, fp);
28    if (ret != sizeToWrite)
29    {
30        printf("Write file error.\n");
31    }
32
33    fclose(fp);
34}
35
36int main()
37  {
38    pthread_t fileThreadID;
39    int ret = pthread_create(&fileThreadID, NULL, fileThreadFunc, NULL);
40    if (ret != 0)
41    {
42        printf("Failed to create fileThread.\n");
43        return -1;
44    }
45
46    int* retval;
47    pthread_join(fileThreadID, &retval);
48
49    //使用r選項,要求文件必須存在
50    FILE* fp = fopen(TIME_FILENAME, "r");
51    if (fp == NULL)
52    {
53        printf("open file error.\n");
54        return -2;
55    }
56
57    char buf[32] = {0};
58    int sizeRead = fread(buf, 1, 32, fp);
59    if (sizeRead == 0)
60    {
61      printf("read file error.\n");
62      return -3;
63    }
64
65    printf("Current Time is: %s.\n", buf);
66
67    return 0;
68}

程序執行結果如下:

[root@localhost threadtest]# ./test
Current Time is: 2018/09/24 21:06:01.
Windows下等待線程結束

Windows下使用API WaitForSingleObjectWaitForMultipleObjects函數,前者用於等待一個線程結束,後者可以同時等待多個線程結束。當然,這兩個函數的作用不僅可以用於等待線程退出,還可以用於等待其他線程同步對象,本文後面的將深入介紹這兩個函數。與linux的pthread_join函數不同,Windows的WaitForSingleObject函數提供了可選擇等待時間的精細控制。

這裏我們僅演示等待線程退出。

WaitForSingleObject函數簽名如下:

DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
  • 參數hHandle是需要等待的對象的句柄,等待線程退出,傳入線程句柄。
  • 參數dwMilliseconds是需要等待的毫秒數,如果使用INFINITE宏,則表示無限等待下去。
  • 返回值:該函數的返回值有點複雜,我們後面文章具體介紹。當dwMilliseconds參數使用INFINITE值,該函數會掛起當前等待線程,直到等待的線程退出後,等待的線程纔會被喚醒,WaitForSingleObject後的程序執行流繼續執行。

我們將上面的linux示例代碼改寫成Windows版本的:

 1#include <stdio.h>
 2#include <string.h>
 3#include <time.h>
 4#include <Windows.h>
 5
 6#define TIME_FILENAME "time.txt"
 7
 8DWORD WINAPI FileThreadFunc(LPVOID lpParameters)
 9  {
10    time_t now = time(NULL);
11    struct tm* t = localtime(&now);
12    char timeStr[32] = { 0 };
13    sprintf_s(timeStr, 32, "%04d/%02d/%02d %02d:%02d:%02d",
14              t->tm_year + 1900,
15              t->tm_mon + 1,
16              t->tm_mday,
17              t->tm_hour,
18              t->tm_min,
19              t->tm_sec);
20    //文件不存在,則創建;存在,則覆蓋。
21    FILE* fp = fopen(TIME_FILENAME, "w");
22    if (fp == NULL)
23    {
24        printf("Failed to create time.txt.\n");
25        return 1;
26    }
27
28    size_t sizeToWrite = strlen(timeStr) + 1;
29    size_t ret = fwrite(timeStr, 1, sizeToWrite, fp);
30    if (ret != sizeToWrite)
31    {
32        printf("Write file error.\n");
33    }
34
35    fclose(fp);
36
37    return 2;
38}
39
40
41int main()
42  {
43    DWORD dwFileThreadID;
44    HANDLE hFileThread = CreateThread(NULL, 0, FileThreadFunc, NULL, 0, 
45                                      &dwFileThreadID);
46    if (hFileThread == NULL)
47    {
48        printf("Failed to create fileThread.\n");
49        return -1;
50    }
51
52    //無限等待,直到文件線程退出,否則程序將一直掛起。
53    WaitForSingleObject(hFileThread, INFINITE);
54
55    //使用r選項,要求文件必須存在
56    FILE* fp = fopen(TIME_FILENAME, "r");
57    if (fp == NULL)
58    {
59        printf("open file error.\n");
60        return -2;
61    }
62
63    char buf[32] = { 0 };
64    int sizeRead = fread(buf, 1, 32, fp);
65    if (sizeRead == 0)
66    {
67        printf("read file error.\n");
68        return -3;
69    }
70
71    printf("Current Time is: %s.\n", buf);
72
73    return 0;
74}

程序執行結果:

C++11提供的等待線程結果函數

可以想到,C++11的std::thread既然統一了linux和Windows的線程創建函數,那麼它應該也提供等待線程退出的接口,確實如此,std::threadjoin方法就是用來等待線程退出的函數。當然使用這個函數時,必須保證該線程還處於運行中狀態,也就是說等待的線程必須是可以”join“的,如果需要等待的線程已經退出,此時調用join方法,程序會產生崩潰。因此,C++11的線程庫同時提供了一個joinable方法來判斷某個線程是否可以等待,如果您不確定您的線程是否可以”join”,可以先調用joinable函數判斷一下是否需要等待。

還是以上面的例子爲例,改寫成C++11的代碼:

 1#include <stdio.h>
 2#include <string.h>
 3#include <time.h>
 4#include <thread>
 5
 6#define TIME_FILENAME "time.txt"
 7
 8void FileThreadFunc()
 9  {
10    time_t now = time(NULL);
11    struct tm* t = localtime(&now);
12    char timeStr[32] = { 0 };
13    sprintf_s(timeStr, 32, "%04d/%02d/%02d %02d:%02d:%02d",
14              t->tm_year + 1900,
15              t->tm_mon + 1,
16              t->tm_mday,
17              t->tm_hour,
18              t->tm_min,
19              t->tm_sec);
20    //文件不存在,則創建;存在,則覆蓋。
21    FILE* fp = fopen(TIME_FILENAME, "w");
22    if (fp == NULL)
23    {
24        printf("Failed to create time.txt.\n");
25        return;
26    }
27
28    size_t sizeToWrite = strlen(timeStr) + 1;
29    size_t ret = fwrite(timeStr, 1, sizeToWrite, fp);
30    if (ret != sizeToWrite)
31    {
32        printf("Write file error.\n");
33    }
34
35    fclose(fp);
36}
37
38int main()
39  {
40    std::thread t(FileThreadFunc);
41    if (t.joinable())
42        t.join();
43
44    //使用r選項,要求文件必須存在
45    FILE* fp = fopen(TIME_FILENAME, "r");
46    if (fp == NULL)
47    {
48        printf("open file error.\n");
49        return -2;
50    }
51
52    char buf[32] = { 0 };
53    int sizeRead = fread(buf, 1, 32, fp);
54    if (sizeRead == 0)
55    {
56        printf("read file error.\n");
57        return -3;
58    }
59
60    printf("Current Time is: %s.\n", buf);
61
62    return 0;
63}

線程函數傳C++類實例指針慣用法

前面的章節介紹了除了C++11的線程庫提供了的std::thread類對線程函數簽名沒有特殊要求外,無論是linux還是Windows的線程函數的簽名都必須是指定的格式,即參數和返回值必須是規定的形式。如果使用C++面向對象的方式對線程函數進行封裝,那麼線程函數就不能是類的實例方法,即必須是靜態方法。那麼,爲什麼不能是類的實例方法呢?我們以linux的線程函數簽名爲例:

void threadFunc(void* arg);

假設,我們將線程的基本功能封裝到一個Thread類中,部分代碼如下:

 1class Thread
 2  {
 3public:
 4    Thread();
 5    ~Thread();
 6
 7    void start();
 8    void stop();
 9
10    void threadFunc(void* arg);
11};

由於threadFunc是一個類實例方法,無論是類的實例方法還是靜態方法,C++編譯器在編譯時都會將這些函數”翻譯“成全局函數,即去掉類的域限制。對於實例方法,爲了保證類方法的正常功能,C++編譯器在翻譯時,會將類的實例對象地址(也就是this指針)作爲類的第一個參數合併給該方法,也就是說,翻譯後的threadFunc的簽名變成了如下形式(僞代碼):

void threadFunc(Thread* this, void* arg);

這樣的話,就不符合線程函數的要求了。因此如果一個線程函數作爲類方法,只能是靜態方法而不能是實例方法。

當然,如果是使用C++11的std::thread類就沒有這個限制,即使類成員函數是類的實例方法也可以,但是必須顯式地將線程函數所屬的類對象實例指針(在類的內部就是this指針)作爲構造函數參數傳遞給std::thread,還是需要傳遞類的this指針,這在本質上是一樣的,代碼實例如下:

 1#include <thread>
 2#include <memory>
 3#include <stdio.h>
 4
 5class Thread
 6  {
 7public:
 8    Thread()
 9    {
10    }
11
12    ~Thread()
13    {
14    }
15
16    void Start()
17      {
18        m_stopped = false;
19        //threadFunc是類的非靜態方法,所以作爲線程函數,第一個參數必須傳遞類實例地址,即this指針
20        m_spThread.reset(new std::thread(&Thread::threadFunc, this, 8888, 9999));
21    }
22
23    void Stop()
24      {
25        m_stopped = true;
26        if (m_spThread)
27        {
28            if (m_spThread->joinable())
29                m_spThread->join();
30        }
31    }
32
33private:
34    void threadFunc(int arg1, int arg2)
35      {
36        while (!m_stopped)
37        {
38            printf("Thread function use instance method.\n");
39        }      
40    }
41
42private:
43    std::shared_ptr<std::thread>  m_spThread;
44    bool                          m_stopped;
45};
46
47int main()
48  {
49    Thread mythread;
50    mythread.Start();
51
52    while (true)
53    {
54        //權宜之計,讓主線程不要提前退出
55    }
56
57    return 0;
58}

上述代碼中使用了C++11新增的智能指針std::shared_ptr類來包裹了一下new出來的std::thread對象,這樣我們就不需要自己手動delete這個的std::thread對象了。

綜上所述,如果不使用C++11的語法,那麼線程函數只能作爲類的靜態方法,且函數簽名必須按規定的簽名格式來。如果是類的靜態方法,那麼就沒法訪問類的實例方法了,爲了解決這個問題,我們在實際開發中往往會在創建線程時將當前對象的地址(this指針)傳遞給線程函數,然後在線程函數中,將該指針轉換成原來的類實例,再通過這個實例就可以訪問類的所有方法了。代碼示例如下:

.h文件代碼如下:

 1/**
 2   * Thread.h
 3   */
 4#ifdef WIN32
 5//#include <windows.h>
 6typedef HANDLE THREAD_HANDLE ;
 7#else
 8//#include <pthread.h>
 9typedef pthread_t THREAD_HANDLE ;
10#endif
11
12/**定義了一個線程對象
13   */
14class  CThread  
15  {
16public:
17    /**構造函數
18       */
19    CThread();
20
21    /**析構函數
22       */
23    virtual ~CThread();
24
25    /**創建一個線程
26       * @return true:創建成功 false:創建失敗
27       */
28    virtual bool Create();
29
30    /**獲得本線程對象存儲的線程句柄
31       * @return 本線程對象存儲的線程句柄線程句柄
32       */
33    THREAD_HANDLE GetHandle();
34
35    /**線程睡眠seconds秒
36       * @param seconds 睡眠秒數
37       */
38    void OSSleep(int nSeconds);
39
40    void SleepMs(int nMilliseconds);
41
42    bool Join();
43
44    bool IsCurrentThread();
45
46    void ExitThread();
47
48private:    
49#ifdef WIN32
50    static DWORD WINAPI _ThreadEntry(LPVOID pParam);
51#else
52    static void* _ThreadEntry(void* pParam);
53#endif
54
55    /**虛函數,子類可做一些實例化工作
56       * @return true:創建成功 false:創建失敗
57       */
58    virtual bool InitInstance();
59
60    /**虛函數,子類清楚實例
61       */
62    virtual void ExitInstance();
63
64    /**線程開始運行,純虛函數,子類必須繼承實現
65       */
66    virtual void Run() = 0;
67
68private:
69     THREAD_HANDLE  m_hThread;  /**< 線程句柄 */
70     DWORD          m_IDThread;
71
72};

.cpp文件如下:

  1/**
  2   * Thread.cpp
  3   */
  4#include "Thread.h"
  5
  6#ifdef WIN32
  7DWORD WINAPI CThread::_ThreadEntry(LPVOID pParam)
  8#else
  9void* CThread::_ThreadEntry(void* pParam)
 10#endif
 11{
 12    CThread *pThread = (CThread *)pParam;
 13    if(pThread->InitInstance())
 14    {
 15        pThread->Run();
 16    }
 17
 18    pThread->ExitInstance();
 19
 20    return NULL;
 21}
 22
 23CThread::CThread()
 24{
 25    m_hThread = (THREAD_HANDLE)0;
 26    m_IDThread = 0;
 27}
 28
 29CThread::~CThread()
 30{
 31}
 32
 33bool CThread::Create()
 34{
 35    if (m_hThread != (THREAD_HANDLE)0)
 36    {
 37        return true;
 38    }
 39    bool ret = true;
 40#ifdef WIN32
 41    m_hThread = ::CreateThread(NULL,0,_ThreadEntry,this,0,&m_IDThread);
 42    if(m_hThread==NULL)
 43    {
 44        ret = false;
 45    }
 46#else
 47    ret = (::pthread_create(&m_hThread,NULL,&_ThreadEntry , this) == 0);
 48#endif
 49    return ret;
 50}
 51
 52bool CThread::InitInstance()
 53{
 54    return true;
 55}
 56
 57void CThread::ExitInstance()
 58{
 59}
 60
 61void CThread::OSSleep(int seconds)
 62{
 63#ifdef WIN32
 64    ::Sleep(seconds*1000);
 65#else
 66    ::sleep(seconds);
 67#endif
 68}
 69
 70void CThread::SleepMs(int nMilliseconds)
 71{
 72#ifdef WIN32
 73    ::Sleep(nMilliseconds);
 74#else
 75    ::usleep(nMilliseconds);
 76#endif
 77}
 78
 79bool CThread::IsCurrentThread()
 80{
 81#ifdef WIN32
 82    return ::GetCurrentThreadId() == m_IDThread;
 83#else
 84    return ::pthread_self() == m_hThread;
 85#endif
 86}
 87
 88bool CThread::Join()
 89{    
 90    THREAD_HANDLE hThread = GetHandle();
 91    if(hThread == (THREAD_HANDLE)0)
 92    {
 93        return true;
 94    }
 95#ifdef WIN32
 96    return (WaitForSingleObject(hThread,INFINITE) != 0);
 97#else
 98    return (pthread_join(hThread, NULL) == 0);
 99#endif
100}
101
102void CThread::ExitThread()
103{
104#ifdef WIN32
105    ::ExitThread(0);
106#else
107#endif
108}

上述代碼CThread類封裝了一個線程的常用的操作,使用宏WIN32來分別實現了Windows和linux兩個操作系統平臺的線程操作。其中InitInstanceExitInstance方法爲虛函數,在繼承CThread的子類中可以改寫這兩個方法,根據實際需要在線程函數正式業務邏輯前後做一些初始化和反初始化工作,而純虛接口Run方法必須改寫,自定成您的線程實際執行函數。

在線程函數中通過在創建線程時(調用CreateThreadpthread_create方法)時,將當前對象的this指針作爲線程的函數的唯一參數傳入,這樣在線程函數中,可以通過線程函數參數得到對象的指針,通過這個指針就可以自由訪問類的實例方法了。這一技巧非常常用,它廣泛地用於各類開源C++項目或者實際的商業C++項目中,希望讀者能理解並熟練掌握它

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