此為牛客Linux C++課程筆記。
0. 關于線程
注意:LWP號和線程id不同, LWP號是CPU分配時間片的依據,線程id是用于在進程內部區分線程的。
1. 線程與進程的區別
對于進程來說,相同的地址(同一個虛擬地址)在不同的進程中,反復使用而不沖突。原因是他們雖虛擬址一樣,但,頁目錄、頁表、物理頁面各不相同。相同的虛擬址,映射到不同的物理頁面內存單元,最終訪問不同的物理頁面。
但!線程不同!兩個線程具有各自獨立的PCB,但共享同一個頁目錄,也就共享同一個頁表和物理頁面。所以兩個PCB共享一個地址空間。
實際上,無論是創建進程的fork,還是創建線程的pthread_create,底層實現都是調用同一個內核函數clone。
如果復制對方的地址空間,那么就產出一個“進程”;如果共享對方的地址空間,就產生一個“線程”。
因此:Linux內核是不區分進程和線程的。只在用戶層面上進行區分。所以,線程所有操作函數 pthread_* 是庫函數,而非系統調用。
優點: 1. 提高程序并發性 2. 開銷小 3. 數據通信、共享數據方便
缺點: 1. 庫函數,不穩定 2. 調試、編寫困難、gdb不支持 3. 對信號支持不好
優點相對突出,缺點均不是硬傷。Linux下由于實現方法導致進程、線程差別不是很大。
2. 線程相關操作函數
2.1 獲取線程id
#include <pthread.h>
pthread_t pthread_self(void);
功能:獲取線程ID。其作用對應進程中 getpid() 函數。
2.2 創建線程: pthread_create
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
功能:創建一個子線程
參數:
- thread:傳出參數,線程創建成功后,子線程的線程ID被寫到該變量中。
- attr : 設置線程的屬性,一般使用默認屬性,即NULL
- start_routine : 函數指針,這個函數是子線程需要處理的邏輯代碼
- arg : 給第三個參數(回調函數)使用,是回調函數的參數
返回值:
- 成功:0
- 失敗:返回錯誤號。這個錯誤號和之前errno不太一樣,獲取錯誤號的信息使用:
#include <string.h>
char * strerror(int errnum);
創建線程示例代碼如下:
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void* callback(void* arg) {printf("its child thread, thread id is %lu\n", pthread_self());printf("arg = %d\n", *(int *)arg);
}int main()
{pthread_t pid;int a = 5;int ret = pthread_create(&pid, NULL, callback, &a);if(ret != 0) {// 說明創建失敗char * errstr = strerror(ret);printf("error: %s\n", errstr);}printf("its main thread, thread id is %lu\n", pthread_self());sleep(1);return 0;
}
發現無法編譯
查閱文檔發現:
編譯時加-pthread即可,運行結果如下:
2.3 終止線程: pthread_exit
注意,不能使用exit函數終止當前線程,exit將終止當前進程,進程中的所有線程將一并終止。
#include <pthread.h>
void pthread_exit(void *retval);
參數:retval表示線程退出狀態,通常傳NULL
多線程環境中,應盡量少用,或者不使用exit函數,取而代之使用pthread_exit函數,將單個線程退出。任何線程里exit導致進程退出,其他線程未工作結束,主控線程退出時不能return或exit。
2.4 連接已終止的線程(回收線程):pthread_join
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能:和一個已經終止的線程進行連接(回收子線程的資源)
注意:這個函數是阻塞函數,調用一次只能回收一個子線程,一般在主線程中使用
參數:
- thread:需要回收的子線程的ID
- retval: 接收子線程退出時的返回值(即pthread_exit的
void *retva
l參數), 而且是傳出參數。
返回值:0 : 成功;非0 : 失敗,返回的錯誤號
不使用傳出參數的一個簡單使用如下:
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void* callback(void* arg) {printf("子線程運行中...\n");sleep(2);
}int main()
{pthread_t pid;int ret = pthread_create(&pid, NULL, callback, NULL);if(ret != 0) {// 說明創建失敗char * errstr = strerror(ret);printf("error: %s\n", errstr);}pthread_join(pid, NULL);printf("子線程已回收\n");return 0;
}
子線程執行2秒后,主進程才輸出“子線程已回收”,說明pthread_join函數是阻塞的。
pthread_join函數比較難以理解的地方是他的第二個參數:void **retval,是void二級指針類型,這是因為:
首先這個參數是想接收pthread_exit所傳出的void *retval
, 這個參數本身是void *的一級指針類型,而pthread_join函數的void **retval在設計時是設計成一個傳出參數的,以便把pthread_exit傳出的void *retval帶回主線程,所以要想把 void * 類型變量設計成傳出參數,即是 void **。
示例程序如下:
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>int value = 10;void* callback(void* arg) {printf("子線程運行中...\n");pthread_exit((void *)&value);
}int main()
{pthread_t pid;int ret = pthread_create(&pid, NULL, callback, NULL);if(ret != 0) {// 說明創建失敗char * errstr = strerror(ret);printf("error: %s\n", errstr);}int *thread_retval; // 給pthread_join調用,接收pthread_exit的傳出參數pthread_join(pid, (void **)&thread_retval);printf("exit data : %d\n", *thread_retval);return 0;
}
運行結果如下:
2.5 線程分離:pthread_detach
#include <pthread.h>
int pthread_detach(pthread_t thread);
功能:使進程處于分離狀態。被分離的線程在終止的時候,會自動釋放資源返回給系統,避免產生僵尸線程。
線程分離狀態:指定該狀態,線程主動與主控線程斷開關系。線程結束后,其退出狀態不由其他線程獲取,而直接自己自動釋放。網絡、多線程服務器常用。
參數:需要分離的線程的ID
返回值:成功:0,失敗:返回錯誤號
注意:
- 線程不能多次分離,會產生不可預料的行為。
- 不能去連接(pthread_join)一個已經分離的線程,會報錯:一般情況下,線程終止后,其終止狀態一直保留到其它線程調用pthread_join獲取它的狀態為止。但是線程也可以被置為detach狀態,這樣的線程一旦終止就立刻回收它占用的所有資源,而不保留終止狀態。不能對一個已經處于detach狀態的線程調用pthread_join,這樣的調用將返回EINVAL錯誤。也就是說,如果已經對一個線程調用了pthread_detach就不能再調用pthread_join了。
2.6 線程取消:pthread_cancel
#include <pthread.h>
int pthread_cancel(pthread_t thread);
功能:取消線程(讓線程終止)
【注意】:線程的取消并不是實時的,而有一定的延時。需要等待線程到達某個取消點(檢查點)。
類似于玩游戲存檔,必須到達指定的場所(存檔點,如:客棧、倉庫、城里等)才能存儲進度。殺死線程也不是立刻就能完成,必須要到達取消點。
取消點:是線程檢查是否被取消,并按請求進行動作的一個位置。通常是一些系統調用creat,open,pause,close,read,write…
執行命令man 7 pthreads可以查看具備這些取消點的系統調用列表。也可參閱 APUE.12.7 取消選項小節。
可粗略認為一個系統調用(進入內核)即為一個取消點。如線程中沒有取消點,可以通過調用pthreestcancel函數自行設置一個取消點。
看下面這個代碼示例,子線程無限循環:
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void* callback(void* arg) {while(1) {printf("子線程運行中...\n");sleep(1);}return NULL;
}int main()
{pthread_t pid;int ret = pthread_create(&pid, NULL, callback, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error: %s\n", errstr);}pthread_cancel(pid);ret = pthread_join(pid, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error: %s\n", errstr);}printf("線程已回收\n");return 0;
}
運行后成功輸出”線程已回收“, 這是因為pthread_cancel終止了子線程的運行,故pthread_join得以執行。
但是如果將子進程中循環語句中的內容去掉:
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void* callback(void* arg) {while(1) {// printf("子線程運行中...\n");// sleep(1);}return NULL;
}int main()
{pthread_t pid;int ret = pthread_create(&pid, NULL, callback, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error: %s\n", errstr);}pthread_cancel(pid);ret = pthread_join(pid, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error: %s\n", errstr);}printf("線程已回收\n");return 0;
}
運行以后發現沒有輸出,主線程阻塞。這是因為子線程的while(1)死循環中沒有任何語句,也就不會執行任何系統調用,也就不會到達任何一個“取消點”,所以子線程并沒有被終止,主線程被阻塞在pthread_join處。而之前的代碼循環語句中的printf會調用系統調用write,所以會到達“取消點”,pthread_join將已經結束的子線程回收。