1.線程實現
線程是在進程的基礎上進一步的抽象,也就是說一個進程分為兩個部分:線程集合和資源集合。線程是進程中的一個動態對象,它應該是一組獨立的指令流,進程中的所有線程將共用進程裡的資源。但是線程應該有自己的私人對象:比如程式計數器、堆棧和寄存器上下文。
線程分為三種類型:
(1).核心線程:
它的建立和撤消是由核心的內部需求來決定的,用來負責執行一個指定的函數,一個核心線程不需要和一個使用者進程聯絡起來。它共用核心的本文段核全域資料,具有自己的核心堆棧。它能夠單獨的被調度並且使用標準的核心同步機制,可以被單獨的分配到一個處理器上運行。核心線程的調度由於不需要經過態的轉換並進行地址空間的重新對應,因此在核心線程間做環境切換比在進程間做環境切換快得多。
(2).輕量級進程:
輕量級進程是核心支援的使用者線程,它在一個單獨的進程中提供多線程式控制制。這些輕量級進程被單獨的調度,可以在多個處理器上運行,每一個輕量級進程都被綁定在一個核心線程上,而且在它的生命週期這種綁定都是有效。輕量級進程被獨立調度並且共用地址空間和進程中的其它資源,但是每個LWP都應該有自己的程式計數器、寄存器集合、核心棧和使用者棧。
(3).使用者線程:
使用者線程是通過線程庫實現的。它們可以在沒有核心參與下建立、釋放和管理。線程庫提供了同步和調度的方法。這樣進程可以使用大量的線程而不消耗核心資源,而且省去大量的系統開銷。使用者線程的實現是可能的,因為使用者線程的上下文可以在沒有核心幹預的情況下儲存和恢複。每個使用者線程都可以有自己的使用者堆棧,一塊用來儲存使用者級寄存器上下文以及如訊號屏蔽等狀態資訊的記憶體區。庫通過儲存當前線程的堆棧和寄存器內容載入新調度線程的那些內容來實現使用者線程之間的調度和環境切換。
核心仍然負責進程的切換,因為只有核心具有修改記憶體管理寄存器的權力。使用者線程不是真正的調度實體,核心對它們一無所知,而只是調度使用者線程下的進程或者輕量級進程,這些進程再通過線程庫函數來調度它們的線程。當一個進程被搶佔時,它的所有使用者線程都被搶佔,當一個使用者線程被阻塞時,它會阻塞下面的輕量級進程,如果進程只有一個輕量級進程,則它的所有使用者線程都會被阻塞。
Linux線程是通過進程來實現。Linux kernel為進程建立提供一個clone()系統調用,clone的參數包括如 CLONE_VM, CLONE_FILES, CLONE_SIGHAND 等。通過clone()的參數,新建立的進程,也稱為LWP(Lightweight process)與父進程共用記憶體空間,檔案控制代碼,訊號處理等,從而達到建立線程相同的目的。
在Linux 2.6之前,Linux kernel並沒有真正的thread支援,一些thread library都是在clone()基礎上的一些基於user space的封裝,因此通常在訊號處理、進程調度(每個進程需要一個額外的調度線程)及多線程之間同步共用資源等方面存在一定問題。Linux 2.6的線程庫叫NPTL(Native POSIX Thread Library)。POSIX thread(pthread)是一個編程規範,通過此規範開發的多線程程式具有良好的跨平台特性。儘管是基於進程的實現,但新版的NPTL建立線程的效率非常高。一些測試顯示,基於NPTL的核心建立10萬個線程只需要2秒,而沒有NPTL支援的核心則需要長達15分鐘。
在Linux中,每一個線程都有一個task_struct。線程和進程可以使用同一調度其調度。核心角度上來將LWP和Process沒有區別,有的僅僅是資源的共用。如果獨享資源則是HWP,共用資源則是LWP。而在真正核心實現的
NPTL的實現是在kernel增加了futex(fast userspace mutex)支援用於處理線程之間的sleep與wake。futex是一種高效的對共用資源互斥訪問的演算法。kernel在裡面起仲裁作用,但通常都由進程自行完成。NPTL是一個1×1的執行緒模式,即一個線程對於一個作業系統的調度進程,優點是非常簡單。而其他一些作業系統比如Solaris則是MxN的,M對應建立的線程數,N對應作業系統可以啟動並執行實體。(N<M),優點是線程切換快,但實現稍複雜。
2.Linux線程庫pthread
(1). 測試宏
#ifdef _POSIX_THREADS
或者sysconf(_SC_THREADS)
來檢測是否支援宏。
(2). 線程標識pthread_t
pit_t是整個系統中唯一的。而線程pthread_t(struct)不同,只在它所屬的進程環境有效。
判斷兩個線程ID是否相等:
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
//傳回值若相等則返回非0值,不相等則返回0
獲得線程自身pthread_t
pthread_t pthread_self(void)
(3).線程建立
程式啟動時,它是以單進程的單個控制進程啟動的。如果要增加新的線程,可以調用pthread_create來建立。
#include <pthread.h>
int pthread_create(
pthread_t *restrict tidp,
/*第一個參數為指向線程標識符的指標*/
const pthread_attr_t *restrict attr,
/*第二個參數用來設定線程屬性*/
void *(*rtn)(void),
/*第三個參數是線程運行函數的起始地址*/
void *restrict arg
/*最後一個參數是運行函數的參數*/
);
//若成功則返回0,否則返回錯誤編號。它不設定errno
C99中的新關鍵字restrict以及受限指標(restricted pointers)。受限指標作為一種編譯器最佳化代碼的方式,是由編譯器廠商提供特定的實現.
restrict只可以用在指標身上。如果一個指標被restrict修飾,那麼就在它(這個指標)和它所指向的對象之間建立了一種特殊的聯絡──只能用這個指標或者這個指標的運算式來訪問這個對象的值.一個指標指向一個記憶體位址。同一塊記憶體可以由多個指標來訪問並在程式運行時修改它(這塊記憶體)。restrict告訴編譯器,如果一塊由一個被restrict修飾的指標所指向的記憶體被修改了,那麼沒有其它的指標可以來訪問這塊記憶體。編譯器可能會選擇一種方式來最佳化代碼中調用被restrict修飾的指標的部分,這可能導致錯誤發生。程式員有責任來確保正確地按照他們所設想的來使用被restrict修飾的指標,否則的話,可能會發生意想不到的結果。
如果一塊特定的記憶體區沒有被修改,那麼它可以被多個restrict指標(被restrict修飾的指標)所指代(或者叫做引用或訪問)。另外,restrict指標的賦值是有限制的,這一點在函數調用和嵌套塊(nested block)之間是沒有區別的。在包含restrict指標的塊中,只能將外層的restrict指標的值賦給內層的restrict指標,在同層內不可以相互賦值,當然在外層不可能賦以內層的值。一個例外是,當一個聲明restrict指標的代碼快執行完後,這個restrict指標所指的記憶體就可以被其它的指標訪問了(因為那個restrict是那個代碼塊的局部變數,當執行完那個代碼塊後,這個指標也就不複存在了)。
(4). 線程終止
如果進程中的任一線程調用了exit, __EXIT虎或_exit,那麼整個進程就會終止。與之類似如果訊號的預設動作是終止進程,那麼訊號發送到線程就會終止整個進程。
線程的退出方式:
(1).線程只從啟動函數中返回,傳回值是線程的退出碼。
(2).線程可以被同一進程中其他線程取消。
(3).線程調用pthread_exit()。
void pthread_exit(void *rval_ptr);
rval_ptr是一個無類型指標,與傳給啟動函數的單個參數類似。進程中其他進程可以調用pthread_join函數訪問到這個指標。如果線程從啟動函數返回,rval_ptr將包含返回碼。如果線程被取消,由rval_ptr指定的記憶體單元就置為PTHREAD_CANCELED。如果對線程的傳回值不感興趣,那麼就可以把rval_ptr設定為NULL。在樣調用pthread_join函數將等待指定的線程終止,但並不擷取線程的終止狀態。例如線上程啟動函數中返回:return ((void *) 1)這樣pthread_join的函數可以通過rval_ptr這個指標得到線程的退出狀態。如果線程調用ptread_exit((void
*2)),那麼pthread_join依然可以得到退出狀態2.這個指標可以是複雜的結構類型以便儲存更多資訊。
int pthread_join(pthread_t thread, void **rval_ptr);
/* 成功返回0,否則返回錯誤編號 */
調用pthread_join的線程將被阻塞,直到指定的線程(thread)調用pthread_exit,從啟動函數或者被取消。另外,void *rval_ptr可以作為一個int直接返回。例如return (void *)1;或者pthread_exit((void *)1);那麼從pthread_join獲得該指標直接可以用(int)ptr來輸出。
這個void *rval_ptr指向的記憶體位址不應該在棧上,其內容會被下面調用的函數覆蓋。應該是在堆上(通過malloc來建立)或者在待用資料段(用靜態,全域變數)。
int pthread_cancel(pthread_t tid);
/* 成功返回0,否則返回錯誤編號 */
行為相等於參數值等同於PTHREAD_CANCELED的pthread_exit函數。
注意:線程可以自行選擇忽略取消或者控製取消方式。注意pthread_cancel並不等待線程終止,僅僅是提出請求。
線程可以自己安排退出時需要執行的函數,這與進程atexit函數安排退出時要執行的函數是類似的。這樣的函數稱為線程清理處理常式。線程可以建立多個線程處理函數,存放在棧中,執行順序與他們註冊的順序相反。
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
pthread_cleanup_pop刪除上一次註冊的線程清理處理常式。如果execute非0,則執行這個程式。
關於參數的使用方法的一個例子:
- void
- cleanup(void *arg)
- {
- printf("cleanup: %s\n", (char *)arg);
- }
-
- void
- thr_fn1(void *arg)
- {
- /* ... */
- pthread_cleanup_push(cleanup, "first cleanup in Thread ");
- pthread_cleanup_push(cleanup, "second cleanup in Thread ");
-
- /* ... */
- pthread_cleanup_pop(0);
- pthread_cleanup_pop(0);
- return ((void *)1);/* 通過返回終止線程的話,清理處理常式就不會調用 */
- /* or pthread_exit((void *)2); 通過pthread_exit就會正常調用 */
- }
-
- int
- main(void)
- {
- /* ... */
- err = pthread_create(&tid, NULL, thr_fn1, (void *)1);
- if (err != 0)
- /* print err and quit*/
- /* ... */
- err = pthread_join(tid1, &tret);
-
- if (err != 0)
- /* print err and quit*/
- /* ... */
-
- return 0;
- }
參考: