首先,編寫一個耗時的單線程程序:
#include<cstdio> #include<unistd.h> int main() { sleep(5); printf("program exited.\n"); }
編譯並運行這段程序,該程序5秒后輸出,sleep期間不再響應其它消息或執行其他操作。為了更好地處理這種耗時的操作,我們需要使用多線程編程。
先從書上抄些東西:
進程和線程都是操作系統的概念。進程是應用程序的執行實例,每個進程是由私有的虛擬地址空間、代碼、數據和其它各種系統資源組成,進程在運行過程中創建的資源隨着進程的終止而被銷毀,所使用的系統資源在進程終止時被釋放或關閉。
線程是進程內部的一個執行單元。系統創建好進程后,實際上就啟動執行了該進程的主執行線程,主執行線程以函數地址形式,比如說main函數,將程序的啟動點提供給操作系統。主執行線程終止了,進程也就隨之終止。
每一個進程至少有一個主執行線程,它無需由用戶去主動創建,是由系統自動創建的。用戶根據需要在應用程序中創建其它線程,多個線程並發地運行於同一個進程中。一個進程中的所有線程都在該進程的虛擬地址空間中,共同使用這些虛擬地址空間、全局變量和系統資源,所以線程間的通訊非常方便,多線程技術的應用也較為廣泛。
多線程可以實現並行處理,避免了某項任務長時間占用CPU時間。要說明的一點是,目前部分的計算機是單處理器(CPU)的,為了運行所有這些線程,操作系統為每個獨立線程安排一些CPU時間,操作系統以輪換方式向線程提供時間片,這就給人一種假象,好象這些線程都在同時運行。由此可見,如果兩個非常活躍的線程為了搶奪對CPU的控制權,在線程切換時會消耗很多的CPU資源,反而會降低系統的性能。這一點在多線程編程時應該注意。
擁有下述特性的程序可以使用線程:
- 工作可以被多個任務同時執行,或者數據可以同時被多個任務操作。
- 阻塞與潛在的長時間I/O等待。
- 在某些地方使用很多CPU循環而其他地方沒有。
- 對異步事件必須響應。
- 一些工作比其他的重要(優先級中斷)。
多線程也可以用於串行程序,模擬並行執行。很好例子就是經典的web瀏覽器,運行在單CPU的電腦上,許多東西可以同時“顯示”出來。
使用線程編程的幾種常見模型:
-
管理者/工作者(Manager/worker):一個單線程,作為管理器將工作分配給其它線程(工作者),典型的,管理器處理所有輸入和分配工作給其它任務。至少兩種形式的manager/worker模型比較常用:靜態worker池和動態worker池。
-
管道(Pipeline):任務可以被划分為一系列子操作,每一個被串行處理,且是被不同的線程並發處理。汽車裝配線可以很好的描述這個模型。比如IDM等下載軟件的文件分塊同時下載。
-
Peer:和manager/worker模型相似,但是主線程在創建了其它線程后,自己也參與工作。
接下來看看實現多線程編程的接口pthread。
一、線程管理
1.創建和結束線程
函數:
int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,(void*)(*start_rtn)(void*),void *arg); void pthread_exit(void* retval); int pthread_attr_init(pthread_attr_t *attr); int pthread_attr_destroy(pthread_attr_t *attr);
創建線程:
最初,main函數包含了一個缺省的線程。其它線程則需要程序員顯式地創建。pthread_create創建一個新線程並使之運行起來。該函數可以在程序的任何地方調用。
pthread_create的參數與返回值:
第一個參數為指向線程標識符的指針。不能設置為NULL。
第二個參數用來設置線程屬性。可設置NULL為缺省值。
第三個參數是線程運行函數的起始地址,即線程將會執行一次的C函數。
最后一個參數是傳遞給運行函數的參數。傳遞時必須轉換成指向void的指針類型。沒有參數傳遞時,可設置為NULL。
若成功則返回0,否則返回出錯編號。
一個進程可以創建的線程最大數量取決於系統實現。
一旦創建,線程就稱為peers,可以創建其它線程。線程之間沒有指定的結構和依賴關系。
有這樣一對問答:
Q:一個線程被創建后,怎么知道操作系統何時調度該線程使之運行?
A:除非使用了線程的調度機制,否則線程何時何地被執行取決於操作系統的實現。強壯的程序應該不依賴於線程執行的順序。
也就是說,多線程程序的運行結果可能是不確定的,因為不知道系統會何時運行該線程。
線程屬性:
線程具有屬性,用pthread_attr_t表示,在對該結構進行處理之前必須進行初始化,在使用后需要對其去除初始化。我們用pthread_attr_init函數對其初始化,用pthread_attr_destroy對其去除初始化。還有其它的一些函數用於查詢和設置線程屬性結構的指定屬性。
線程屬性結構如下:
typedef struct { int etachstate; //線程的分離狀態 int schedpolicy; //線程調度策略 structsched_param schedparam; //線程的調度參數 int inheritsched; //線程的繼承性 int scope; //線程的作用域 size_t guardsize; //線程棧末尾的警戒緩沖區大小 int stackaddr_set; //線程的棧設置 void* stackaddr; //線程棧的位置 size_t stacksize; //線程棧的大小 }pthread_attr_t;
調用pthread_attr_init之后,pthread_t結構所包含的內容就是操作系統實現支持的線程所有屬性的默認值。
如果要去除對pthread_attr_t結構的初始化,可以調用pthread_attr_destroy函數。如果pthread_attr_init實現時為屬性對象分配了動態內存空間,pthread_attr_destroy還會用無效的值初始化屬性對象,因此如果經pthread_attr_destroy去除初始化之后的pthread_attr_t結構被pthread_create函數調用,將會導致其返回錯誤。
線程屬性的其他特性和用法之后再討論。
結束終止:
結束線程的方法有一下幾種:
- 線程從主線程(main函數的初始線程)返回。
- 線程調用了pthread_exit函數。
- 其它線程使用 pthread_cancel函數結束線程。
- 調用exec或者exit函數,整個進程結束。
pthread_exit用於顯式退出線程。典型地,pthread_exit()函數在線程完成工作時或不再需要時候被調用,退出線程。如果main函數在調用了pthread_exit()后將退出,盡管在main中已經沒有可執行的代碼了,進程和所有線程將保持存活狀態,其他線程將會繼續執行。否則,它們會隨着main的結束而終止。對於正常退出,可以免於調用pthread_exit(),除非你想得到一個返回值。程序員可以可選擇地指定終止狀態,當任何線程連接(join)該線程時,該狀態就返回給連接(join)該線程的線程。pthread_exit()函數並不會關閉文件,任何在線程中打開的文件將會一直處於打開狀態,直到線程結束。
現在我們使用多線程來使我們的程序能在sleep期間執行其他操作。
#include<cstdio> #include<pthread.h> #include<unistd.h> void* printhello(void*) { for(int i=1;i<=4;i++) { sleep(1); printf("hello! %d sec has past\n",i); } } int main() { pthread_t tid; pthread_create(&tid,NULL,printhello,NULL); sleep(5); printf("program exited.\n"); }
該程序利用多線程,在main函數sleep的時候還能進行輸出。
如果我們在main中使用pthread_exit()退出
#include<cstdio> #include<pthread.h> #include<unistd.h> void* printhello(void*) { for(int i=1;i<=4;i++) { sleep(1); printf("hello! %d sec has past\n",i); } } int main() { pthread_t tid; pthread_create(&tid,NULL,printhello,NULL); pthread_exit(NULL); //退出main的線程 printf("program exited.\n"); //這段代碼將不被執行 }
會發現子線程仍然能繼續執行,如果將
pthread_exit(NULL); //退出main的線程
改為
return 0;
程序將會直接終止,不輸出。
向線程傳遞參數
pthread_create()函數允許程序員向線程的start_rtn函數傳遞一個參數。當多個參數需要被傳遞時,可以通過定義一個結構體包含所有要傳的參數,然后用pthread_create()傳遞一個指向改結構體的指針,來打破傳遞參數的個數的限制。 所有參數都應該傳引用傳遞並轉化成(void*)。
要安全地向一個新創建的線程傳遞數據應確保所傳遞的數據是線程安全的(不能被其他線程修改)。
下面的代碼片段演示了如何向一個線程傳遞一個簡單的整數。
#include<cstdio> #include<pthread.h> #include<unistd.h> void* printid(void* id) { printf("my thread id is %d\n",*(int*)id); } int main() { pthread_t tid; pthread_create(&tid,NULL,printid,(void*)tid); pthread_exit(NULL);//不使用return是因為主函數退出太快以至於子線程沒輸出就被終止了 }
下面的代碼片段演示了如何向一個線程傳遞結構體參數。
#include<cstdio> #include<pthread.h> #include<unistd.h> struct point { int x,y; }; void* print(void* p) { printf("I got a point (%d,%d).\n",((point*)p)->x,((point*)p)->y); } int main() { pthread_t tid[10]; point p[10]; for(int i=0;i<10;i++) { p[i].x=p[i].y=i; pthread_create(&tid[i],NULL,print,(void*)&p[i]); } pthread_exit(NULL); }
而下面的代碼是錯誤的,循環會使線程訪問傳遞給線程的地址前改變參數的值,輸出的結果會有一樣的。
#include<cstdio> #include<pthread.h> #include<unistd.h> void* print(void* i) { printf("i got a number %d\n",*(int*)i); } int main() { pthread_t tid[10]; for(int i=0;i<10;i++) { pthread_create(&tid[i],NULL,print,(void*)&i); } pthread_exit(NULL); }
main函數中調用pthread_exit的時候,進程不會退出,所以main創建的線程就不會退出,但是main的局部變量存儲的堆棧會被釋放。
#include<cstdio> #include<pthread.h> #include<unistd.h> void* print(void* i) { printf("i got a number %d\n",*(int*)i); sleep(10); printf("i got a number %d\n",*(int*)i); } int main() { pthread_t tid[10]; for(int i=0;i<10;i++) { pthread_create(&tid[i],NULL,print,(void*)&i); } pthread_exit(NULL); }
這段程序的子線程第一次輸出和10s后的輸出結果是不同的,局部變量會在pthread_exit執行完后被釋放。
2.連接(Joining)和分離(Detaching)線程
函數:
int pthread_join (pthread_t tid, void **thread_return); int pthread_detach (pthread_t tid); int pthread_attr_setdetachstate (pthread_attr_t *attr,int detachstate); int pthread_attr_getdetachstate (const pthread_attr_t *attr,int *detachstate);
連接:
“連接”是一種在線程間完成同步的方法。例如:
pthread_join()函數阻塞調用線程直到tid所指定的線程終止。
代碼中如果沒有pthread_join()函數,主函數會很快結束從而使整個進程結束,從而使創建的線程沒有機會開始執行就結束了。加入pthread_join()函數后,主線程會一直等待直到等待的線程結束自己才結束,使創建的線程有機會執行。
如果在目標線程中調用pthread_exit(),程序員可以在主線程中獲得目標線程的終止狀態。連接線程只能用pthread_join()連接一次。若多次調用就會發生邏輯錯誤。
還有兩種同步方法,互斥量(mutexes)和條件變量(condition variables),稍后討論。
線程具有可連接性:
當一個線程被創建,它有一個屬性定義了它是可連接的(joinable)還是分離的(detached)。只有是可連接的線程才能被連接(joined),若果創建的線程是分離的,則不能連接。
使用pthread_create()的attr參數可以顯式的創建可連接或分離的線程,典型四步如下:
- 聲明一個pthread_attr_t數據類型的線程屬性變量
- 用pthread_attr_init()初始化改屬性變量
- 用pthread_attr_setdetachstate()設置可分離狀態屬性
- 之后,用pthread_attr_destroy()釋放屬性所占用的庫資源
分離:
pthread_detach()可以顯式用於分離線程,盡管創建時是可連接的。
pthread_detach(pthread_self());
或在主線程中調用
pthread_detach(thread_id);//非阻塞,可立即返回
這將使該子線程的狀態設置為detached,則該線程運行結束后會自動釋放所有資源。
建議:
- 若線程需要連接,考慮創建時將線程顯式設置為可連接的。因為並非所有創建線程的實現都是默認將線程創建為可連接的。
- 若事先知道線程從不需要連接,考慮創建線程時將其設置為可分離狀態。
下面的代碼演示了使用pthread_join()
#include <pthread.h> #include <cstdio> #include <unistd.h> void* print1 (void*) { printf("x"); } void* print2 (void*) { printf("y"); } int main () { pthread_t tid1,tid2; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_JOINABLE); pthread_create(&tid1,&attr,&print1,NULL); pthread_create(&tid2,&attr,&print2,NULL); pthread_join(tid1,NULL); pthread_join(tid2,NULL); return 0; }
輸出結果是不能確定的,pthread_join阻塞的是調用join函數的所在main線程,其目的是等待並確認t1、t2線程執行結束,但它無法保證多線程的先后執行順序。它只確定被join的線程已經結束”,和線程執行順序沒關系,類似於進程的waitpid函數。能控制線程執行順序的是線程的Mutex和Condition Variables。
每個進程創建以后都應該調用pthread_join或pthread_detach函數,只有這樣在線程結束的時候資源(線程的描述信息和stack)才能被釋放。在新線程里面沒有調用pthread_join或pthread_detach會導致內存泄漏, 如果你創建的線程越多,你的內存利用率就會越高, 直到你再無法創建線程,最終只能結束進程。
- 線程里面調用 pthread_detach(pthread_self()) 這個方法最簡單
- 在創建線程的設置PTHREAD_CREATE_DETACHED屬性
- 創建線程后用 pthread_join() 一直等待子線程結束。
#include<pthread.h> #include<cstdio> #include<unistd.h> void* print(void *) { static int g=0; printf("%d\n", g++); pthread_exit(0); } int main() { pthread_t tid; while(1) { pthread_create(&tid,NULL,print,NULL); pthread_detach(tid); } return 0; }
還剩下一些內容以后再寫......