Linux多線程實踐(二)線程基本API(POSIX)
我們知道,進程在各自獨立的地址空間中運行,進程之間共用資料需要用處理序間通訊機制,有些情況需要在一個進程中同時執行多個控制流程程,這時候線程就派上了用場,比如實現一個圖形介面的下載軟體,一方面需要和使用者互動,等待和處理使用者的滑鼠鍵盤事件,另一方面又需要同時下載多個檔案,等待和處理從多個網路主機發來的資料,這些任務都需要一個“等待-處理”的迴圈,可以用多線程實現,一個線程專門負責與使用者互動,另外幾個線程每個線程負責和一個網路主機通訊。
註:linux 2.6 以後的線程就是由使用者態的pthread庫實現的.使用pthread以後, 在使用者看來, 每一個task_struct就對應一個線程, 而一組線程以及它們所共同引用的一組資源就是一個進程.在linux 2.6中, 核心有了線程組的概念, task_struct結構中增加了一個tgid(thread group id)欄位. getpid(擷取進程ID)系統調用返回的也是tast_struct中的tgid, 而tast_struct中的pid則由gettid系統調用來返回。 當線程停止/繼續, 或者是收到一個致命訊號時, 核心會將處理動作施加到整個線程組中。
比如程式a.out運行時,建立了一個線程。假設主線程的pid是10001、子線程是10002(它們的tgid都是10001)。這時如果你kill 10002,是可以把10001和10002這兩個線程一起殺死的,儘管執行ps命令的時候根本看不到10002這個進程。如果你不知道linux線程背後的故事,肯定會覺得非常奇怪。
與線程有關的函數構成了一個完整的系列,絕大多數函數的名字都是以“pthread_”開頭,要使用這些函數庫,要通過引入頭文,而且連結這些線程函數庫時要使用編譯器命令的“-lpthread”選項[Ubuntu系列系統需要添加的是”-pthread”選項而不是”-lpthread”,如Ubuntu 14.04版本,深度Ubuntu等] 下面開始介紹posix線程基本的API:
pthread_create
int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void*), void *restrict arg);
建立一個新的線程
參數
thread:線程ID
attr:設定線程的屬性,一般設定為NULL表示使用預設屬性
start_routine:是個函數地址,線程啟動後要執行的函數
arg:傳給線程啟動函數的參數
傳回值:成功返回0;失敗返回錯誤碼;
以前學過的系統函數都是成功返回0,失敗返回-1,而錯誤號碼儲存在全域變數errno中,而pthread庫的函數都是通過傳回值返回錯誤號碼,雖然每個線程也都有一個errno,但這是為了相容其它函數介面而提供的,pthread庫本身並不使用它,通過傳回值返回錯誤碼更加清晰。由於pthread_create的錯誤碼不儲存在errno中,因此不能直接用perror(3)列印錯誤資訊,可以先用strerror(3)把錯誤號碼轉換成錯誤資訊再列印。讀取傳回值要比讀取線程內的errno變數的開銷更小!
/** 實踐: 新的錯誤檢查與錯誤退出函數 **/ inline void err_check(const std::string &msg, int retno) { if (retno != 0) err_exit(msg, retno); } inline void err_exit(const std::string &msg, int retno) { std::cerr << msg << ": " << strerror(retno) << endl; exit(EXIT_FAILURE); }
pthread_exit
void pthread_exit(void *value_ptr);
value_ptr:value_ptr不要指向一個局部變數,因為當其它線程得到這個返回指標時線程函數已經退出了。
傳回值:無傳回值,跟進程一樣,線程結束的時候無法返回到它的調用者(自身)
如果需要只終止某個線程而不終止整個進程,可以有三種方法:
1、從線程函數return。這種方法對主線程不適用,從main函數return相當於調用exit,而如果任意一個線程調用了exit或_exit,則整個進程的所有線程都終止。
2、一個線程可以調用pthread_cancel 終止同一進程中的另一個線程。
3、線程可以調用pthread_exit終止自己。
pthread_join
int pthread_join(pthread_t thread, void **value_ptr);
當pthread_create 中的 start_routine返回時,這個線程就退出了,其它線程可以調用pthread_join得到start_routine的傳回值,類似於父進程調用wait(2)得到子進程的退出狀態。
調用該函數的線程將掛起等待,直到id為thread的線程終止。thread線程以不同的方法終止,通過pthread_join得到的終止狀態是不同的,總結如下:
1、如果thread線程通過return返回,value_ptr所指向的單元裡存放的是thread線程函數的傳回值。
2、如果thread線程被別的線程調用pthread_cancel異常終止掉,value_ptr所指向的單元裡存放的是常數PTHREAD_CANCELED。
3、如果thread線程是自己調用pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的參數。
如果對thread線程的終止狀態不感興趣,可以傳NULL給value_ptr參數。
/** 樣本: 等待線程退出 **/ void *thread_rotine(void *args) { for (int i = 0; i < 10; ++i) { printf("B"); fflush(stdout); usleep(20); } pthread_exit(NULL); } int main() { pthread_t thread; int ret = pthread_create(&thread, NULL, thread_rotine, NULL); err_check("pthread_create", ret); for (int i = 0; i < 10; ++i) { printf("A"); fflush(stdout); usleep(20); } ret = pthread_join(thread, NULL); err_check("pthread_join", ret); putchar('\n'); return 0; } pthread_self
pthread_t pthread_self(void);
返回線程ID
在Linux上,pthread_t類型是一個地址值,屬於同一進程的多個線程調用getpid(2)可以得到相同的進程號,而調用pthread_self(3)得到的線程號各不相同。線程id只在當前進程中保證是唯一的,在不同的系統中pthread_t這個類型有不同的實現,它可能是一個整數值,也可能是一個結構體,也可能是一個地址,所以不能簡單地當成整數用printf列印。
/** 樣本:主控線程與子線程傳遞資料 **/ typedef struct _Student { char name[20]; unsigned int age; } Student; void *threadFunction(void *args) { cout << "In Thread: " << pthread_self() << endl; Student tmp = *(Student *)(args); cout << "Name: " << tmp.name << endl; cout << "Age: " << tmp.age << endl; pthread_exit(NULL); } int main() { Student student = {"tach",22}; pthread_t thread; //啟動建立並啟動線程 pthread_create(&thread,NULL,threadFunction,&student); //等待線程結束 pthread_join(thread,NULL); return 0; }
pthread_cancel
int pthread_cancel(pthread_t thread);
線程取消的方法是向目標線程發Cancel訊號,但如何處理Cancel訊號則由目標線程自己決定,或者忽略、或者立即終止、或者繼續運行至Cancelation-point(取消點),由不同的Cancelation狀態決定。
pthread_detach
int pthread_detach(pthread_t thread);
一般情況下,線程終止後,其終止狀態一直保留到其它線程調用pthread_join擷取它的狀態為止(僵線程)。但是線程也可以被置為detach狀態,這樣的線程一旦終止就立刻回收它佔用的所有資源,而不保留終止狀態。不能對一個已經處於detach狀態的線程調用pthread_join,這樣的調用將返回EINVAL。對一個尚未detach的線程調用pthread_join或pthread_detach都可以把該線程置為detach狀態,也就是說,不能對同一線程調用兩次pthread_join,或者如果已經對一個線程調用了pthread_detach就不能再調用pthread_join了。
這個函數既可以在主線程中調用,也可以在thread_function裡面調用。
總結:進程 VS. 線程
進程(pid_t) |
線程(pthread_t) |
Fork |
Pthread_create |
Waitpit |
Pthread_join/Pthread_detach |
Kill |
Pthread_cancel |
Pid |
Pthead_self |
Exit/return |
Pthread_exit/return |
殭屍進程(沒有調用wait/waitpid等函數) |
殭屍線程(沒有調用pthread_join/pthread_detach) |
/** 將並發echo server改造成多線程形式 **/void echo_server(int clientSocket); void *thread_routine(void *arg); int main() { int sockfd = socket(AF_INET,SOCK_STREAM,0); if (sockfd == -1) err_exit("socket error"); int optval = 1; if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval)) == -1) err_exit("setsockopt error"); struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(8002); serverAddr.sin_addr.s_addr = INADDR_ANY; //綁定原生任意一個IP地址 if (bind(sockfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) == -1) err_exit("bind error"); if (listen(sockfd,SOMAXCONN) == -1) err_exit("listen error"); while (true) { int peerSockfd = accept(sockfd, NULL, NULL); if (peerSockfd == -1) err_exit("accept error"); pthread_t tid; /**注意: 下面這種用法可能會產生問題 當另一個串連快讀快速到達, peerSockfd的內容更改, 新建立的線程尚未將該值取走時,線程讀取的就不是 我們原來想讓線程讀取的值了 int ret = pthread_create(&tid, NULL, thread_routine, (void *)&peerSockfd); **/ //解決方案: 為每一個連結建立一塊記憶體 ,注意之後要釋放 int *p = new int(peerSockfd); int ret = pthread_create(&tid, NULL, thread_routine, p); if (ret != 0) err_thread("pthread_create error", ret); } close(sockfd); }
void *thread_routine(void *args) { //將線程設定分離狀態, 避免出現殭屍線程 pthread_detach(pthread_self()); int peerSockfd = *(int *)args; //注意函數中指標取出之後記得將記憶體釋放掉 delete (int *)args; echo_server(peerSockfd); cout << "thread " << pthread_self() << " exiting ..." << endl; pthread_exit(NULL); } void echo_server(int clientSocket) { char buf[BUFSIZ] = {0}; int readBytes; while ((readBytes = read(clientSocket, buf, sizeof(buf))) >= 0) { if (readBytes == 0) { cerr << "client connect closed" << endl; break; } if (write(clientSocket, buf, readBytes) == -1) { cerr << "server thread write error" << endl; break; } cout << buf; bzero(buf, sizeof(buf)); } }