Linux多線程編程

 

一:Linux多線程編程:

 

        線程(thread)技術早在60年代就被提出,但真正應用多線程到操作系統中去,是在80年代中期,solaris是這方面的佼佼者。傳統的Unix也支持線程的概念,但是在一個進程(process)中只允許有一個線程,這樣多線程就意味着多進程。現在,多線程技術已經被許多操作系統所支持,包括Windows/NT,當然,也包括Linux。
  爲什麼有了進程的概念後,還要再引入線程呢?使用多線程到底有哪些好處?什麼的系統應該選用多線程?我們首先必須回答這些問題。
  使用多線程的理由之一是和進程相比,它是一種非常"節儉"的多任務操作方式。我們知道,在Linux系統下,啓動一個新的進程必須分配給它獨立的地址空間,建立衆多的數據表來維護它的代碼段、堆棧段和數據段,這是一種"昂貴"的多任務工作方式。而運行於一個進程中的多個線程,它們彼此之間使用相同的地址空間,共享大部分數據,啓動一個線程所花費的空間遠遠小於啓動一個進程所花費的空間,而且,線程間彼此切換所需的時間也遠遠小於進程間切換所需要的時間。據統計,總的說來,一個進程的開銷大約是一個線程開銷的30倍左右,當然,在具體的系統上,這個數據可能會有較大的區別。
  使用多線程的理由之二是線程間方便的通信機制。對不同進程來說,它們具有獨立的數據空間,要進行數據的傳遞只能通過通信的方式進行,這種方式不僅費時,而且很不方便。線程則不然,由於同一進程下的線程之間共享數據空間,所以一個線程的數據可以直接爲其它線程所用,這不僅快捷,而且方便。當然,數據的共享也帶來其他一些問題,有的變量不能同時被兩個線程所修改,有的子程序中聲明爲static的數據更有可能給多線程程序帶來災難性的打擊,這些正是編寫多線程程序時最需要注意的地方。
  除了以上所說的優點外,不和進程比較,多線程程序作爲一種多任務、併發的工作方式,當然有以下的優點:
  1) 提高應用程序響應。這對圖形界面的程序尤其有意義,當一個操作耗時很長時,整個系統都會等待這個操作,此時程序不會響應鍵盤、鼠標、菜單的操作,而使用多線程技術,將耗時長的操作(time consuming)置於一個新的線程,可以避免這種尷尬的情況。
  2) 使多CPU系統更加有效。操作系統會保證當線程數不大於CPU數目時,不同的線程運行於不同的CPU上。
  3) 改善程序結構。一個既長又複雜的進程可以考慮分爲多個線程,成爲幾個獨立或半獨立的運行部分,這樣的程序會利於理解和修改。
  下面我們先來嘗試編寫一個簡單的多線程程序。

2 簡單的多線程編程
  Linux系統下的多線程遵循POSIX線程接口,稱爲pthread。編寫Linux下的多線程程序,需要使用頭文件pthread.h,連接時需要使用庫libpthread.a。順便說一下,Linux下pthread的實現是通過系統調用clone()來實現的。clone()是Linux所特有的系統調用,它的使用方式類似fork,關於clone()的詳細情況,有興趣的讀者可以去查看有關文檔說明。下面我們展示一個最簡單的多線程程序example1.c。


/* example.c*/
#include <stdio.h>
#include <pthread.h>
void thread(void)
{
int i;
for(i=0;i<3;i++)
printf("This is a pthread./n");
}

int main(void)
{
pthread_t id;
int i,ret;
ret=pthread_create(&id,NULL,(void *) thread,NULL);
if(ret!=0){
printf ("Create pthread error!/n");
exit (1);
}
for(i=0;i<3;i++)
printf("This is the main process./n");
pthread_join(id,NULL);
return (0);
}

我們編譯此程序:
gcc example1.c -lpthread -o example1
運行example1,我們得到如下結果:
This is the main process.
This is a pthread.
This is the main process.
This is the main process.
This is a pthread.
This is a pthread.
再次運行,我們可能得到如下結果:
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.

  前後兩次結果不一樣,這是兩個線程爭奪CPU資源的結果。上面的示例中,我們使用到了兩個函數,  pthread_create和pthread_join,並聲明瞭一個pthread_t型的變量。
  pthread_t在頭文件/usr/include/bits/pthreadtypes.h中定義:
  typedef unsigned long int pthread_t;
  它是一個線程的標識符。函數pthread_create用來創建一個線程,它的原型爲:
  extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
  void *(*__start_routine) (void *), void *__arg));
  第一個參數爲指向線程標識符的指針,第二個參數用來設置線程屬性,第三個參數是線程運行函數的起始地址,最後一個參數是運行函數的參數。這裏,我們的函數thread不需要參數,所以最後一個參數設爲空指針。第二個參數我們也設爲空指針,這樣將生成默認屬性的線程。對線程屬性的設定和修改我們將在下一節闡述。當創建線程成功時,函數返回0,若不爲0則說明創建線程失敗,常見的錯誤返回代碼爲EAGAIN和EINVAL。前者表示系統限制創建新的線程,例如線程數目過多了;後者表示第二個參數代表的線程屬性值非法。創建線程成功後,新創建的線程則運行參數三和參數四確定的函數,原來的線程則繼續運行下一行代碼。
  函數pthread_join用來等待一個線程的結束。函數原型爲:
  extern int pthread_join __P ((pthread_t __th, void **__thread_return));
  第一個參數爲被等待的線程標識符,第二個參數爲一個用戶定義的指針,它可以用來存儲被等待線程的返回值。這個函數是一個線程阻塞的函數,調用它的函數將一直等待到被等待的線程結束爲止,當函數返回時,被等待線程的資源被收回。一個線程的結束有兩種途徑,一種是象我們上面的例子一樣,函數結束了,調用它的線程也就結束了;另一種方式是通過函數pthread_exit來實現。它的函數原型爲:
  extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
  唯一的參數是函數的返回代碼,只要pthread_join中的第二個參數thread_return不是NULL,這個值將被傳遞給thread_return。最後要說明的是,一個線程不能被多個線程等待,否則第一個接收到信號的線程成功返回,其餘調用pthread_join的線程則返回錯誤代碼ESRCH。
  在這一節裏,我們編寫了一個最簡單的線程,並掌握了最常用的三個函數pthread_create,pthread_join和pthread_exit。下面,我們來了解線程的一些常用屬性以及如何設置這些屬性。

3 修改線程的屬性
  在上一節的例子裏,我們用pthread_create函數創建了一個線程,在這個線程中,我們使用了默認參數,即將該函數的第二個參數設爲NULL。的確,對大多數程序來說,使用默認屬性就夠了,但我們還是有必要來了解一下線程的有關屬性。
  屬性結構爲pthread_attr_t,它同樣在頭文件/usr/include/pthread.h中定義,喜歡追根問底的人可以自己去查看。屬性值不能直接設置,須使用相關函數進行操作,初始化的函數爲pthread_attr_init,這個函數必須在pthread_create函數之前調用。屬性對象主要包括是否綁定、是否分離、堆棧地址、堆棧大小、優先級。默認的屬性爲非綁定、非分離、缺省1M的堆棧、與父進程同樣級別的優先級。
  關於線程的綁定,牽涉到另外一個概念:輕進程(LWP:Light Weight Process)。輕進程可以理解爲內核線程,它位於用戶層和系統層之間。系統對線程資源的分配、對線程的控制是通過輕進程來實現的,一個輕進程可以控制一個或多個線程。默認狀況下,啓動多少輕進程、哪些輕進程來控制哪些線程是由系統來控制的,這種狀況即稱爲非綁定的。綁定狀況下,則顧名思義,即某個線程固定的"綁"在一個輕進程之上。被綁定的線程具有較高的響應速度,這是因爲CPU時間片的調度是面向輕進程的,綁定的線程可以保證在需要的時候它總有一個輕進程可用。通過設置被綁定的輕進程的優先級和調度級可以使得綁定的線程滿足諸如實時反應之類的要求。
  設置線程綁定狀態的函數爲pthread_attr_setscope,它有兩個參數,第一個是指向屬性結構的指針,第二個是綁定類型,它有兩個取值:PTHREAD_SCOPE_SYSTEM(綁定的)和PTHREAD_SCOPE_PROCESS(非綁定的)。下面的代碼即創建了一個綁定的線程。
#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;

/*初始化屬性值,均設爲默認值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);

pthread_create(&tid, &attr, (void *) my_function, NULL);

  線程的分離狀態決定一個線程以什麼樣的方式來終止自己。在上面的例子中,我們採用了線程的默認屬性,即爲非分離狀態,這種情況下,原有的線程等待創建的線程結束。只有當pthread_join()函數返回時,創建的線程纔算終止,才能釋放自己佔用的系統資源。而分離線程不是這樣子的,它沒有被其他的線程所等待,自己運行結束了,線程也就終止了,馬上釋放系統資源。程序員應該根據自己的需要,選擇適當的分離狀態。設置線程分離狀態的函數爲pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二個參數可選爲PTHREAD_CREATE_DETACHED(分離線程)和 PTHREAD _CREATE_JOINABLE(非分離線程)。這裏要注意的一點是,如果設置一個線程爲分離線程,而這個線程運行又非常快,它很可能在pthread_create函數返回之前就終止了,它終止以後就可能將線程號和系統資源移交給其他的線程使用,這樣調用pthread_create的線程就得到了錯誤的線程號。要避免這種情況可以採取一定的同步措施,最簡單的方法之一是可以在被創建的線程裏調用pthread_cond_timewait函數,讓這個線程等待一會兒,留出足夠的時間讓函數pthread_create返回。設置一段等待時間,是在多線程編程裏常用的方法。但是注意不要使用諸如wait()之類的函數,它們是使整個進程睡眠,並不能解決線程同步的問題。
  另外一個可能常用的屬性是線程的優先級,它存放在結構sched_param中。用函數pthread_attr_getschedparam和函數pthread_attr_setschedparam進行存放,一般說來,我們總是先取優先級,對取得的值修改後再存放回去。下面即是一段簡單的例子。
#include <pthread.h>
#include <sched.h>
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;

pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr, &param);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, &param);
pthread_create(&tid, &attr, (void *)myfunction, myarg);
  
4 線程的數據處理
  和進程相比,線程的最大優點之一是數據的共享性,各個進程共享父進程處沿襲的數據段,可以方便的獲得、修改數據。但這也給多線程編程帶來了許多問題。我們必須當心有多個不同的進程訪問相同的變量。許多函數是不可重入的,即同時不能運行一個函數的多個拷貝(除非使用不同的數據段)。在函數中聲明的靜態變量常常帶來問題,函數的返回值也會有問題。因爲如果返回的是函數內部靜態聲明的空間的地址,則在一個線程調用該函數得到地址後使用該地址指向的數據時,別的線程可能調用此函數並修改了這一段數據。在進程中共享的變量必須用關鍵字volatile來定義,這是爲了防止編譯器在優化時(如gcc中使用-OX參數)改變它們的使用方式。爲了保護變量,我們必須使用信號量、互斥等方法來保證我們對變量的正確使用。下面,我們就逐步介紹處理線程數據時的有關知識。

4.1 線程數據
  在單線程的程序裏,有兩種基本的數據:全局變量和局部變量。但在多線程程序裏,還有第三種數據類型:線程數據(TSD: Thread-Specific Data)。它和全局變量很象,在線程內部,各個函數可以象使用全局變量一樣調用它,但它對線程外部的其它線程是不可見的。這種數據的必要性是顯而易見的。例如我們常見的變量errno,它返回標準的出錯信息。它顯然不能是一個局部變量,幾乎每個函數都應該可以調用它;但它又不能是一個全局變量,否則在A線程裏輸出的很可能是B線程的出錯信息。要實現諸如此類的變量,我們就必須使用線程數據。我們爲每個線程數據創建一個鍵,它和這個鍵相關聯,在各個線程裏,都使用這個鍵來指代線程數據,但在不同的線程裏,這個鍵代表的數據是不同的,在同一個線程裏,它代表同樣的數據內容。
  和線程數據相關的函數主要有4個:創建一個鍵;爲一個鍵指定線程數據;從一個鍵讀取線程數據;刪除鍵。
  創建鍵的函數原型爲:
  extern int pthread_key_create __P ((pthread_key_t *__key,
  void (*__destr_function) (void *)));
  第一個參數爲指向一個鍵值的指針,第二個參數指明瞭一個destructor函數,如果這個參數不爲空,那麼當每個線程結束時,系統將調用這個函數來釋放綁定在這個鍵上的內存塊。這個函數常和函數pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,爲了讓這個鍵只被創建一次。函數pthread_once聲明一個初始化函數,第一次調用pthread_once時它執行這個函數,以後的調用將被它忽略。

  在下面的例子中,我們創建一個鍵,並將它和某個數據相關聯。我們要定義一個函數createWindow,這個函數定義一個圖形窗口(數據類型爲Fl_Window *,這是圖形界面開發工具FLTK中的數據類型)。由於各個線程都會調用這個函數,所以我們使用線程數據。
/* 聲明一個鍵*/
pthread_key_t myWinKey;
/* 函數 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t once= PTHREAD_ONCE_INIT;
/* 調用函數createMyKey,創建鍵*/
pthread_once ( & once, createMyKey) ;
/*win指向一個新建立的窗口*/
win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
/* 對此窗口作一些可能的設置工作,如大小、位置、名稱等*/
setWindow(win);
/* 將窗口指針值綁定在鍵myWinKey上*/
pthread_setpecific ( myWinKey, win);
}

/* 函數 createMyKey,創建一個鍵,並指定了destructor */
void createMyKey ( void ) {
pthread_keycreate(&myWinKey, freeWinKey);
}

/* 函數 freeWinKey,釋放空間*/
void freeWinKey ( Fl_Window * win){
delete win;
}

  這樣,在不同的線程中調用函數createMyWin,都可以得到在線程內部均可見的窗口變量,這個變量通過函數pthread_getspecific得到。在上面的例子中,我們已經使用了函數pthread_setspecific來將線程數據和一個鍵綁定在一起。這兩個函數的原型如下:
  extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
  extern void *pthread_getspecific __P ((pthread_key_t __key));
  這兩個函數的參數意義和使用方法是顯而易見的。要注意的是,用pthread_setspecific爲一個鍵指定新的線程數據時,必須自己釋放原有的線程數據以回收空間。這個過程函數pthread_key_delete用來刪除一個鍵,這個鍵佔用的內存將被釋放,但同樣要注意的是,它只釋放鍵佔用的內存,並不釋放該鍵關聯的線程數據所佔用的內存資源,而且它也不會觸發函數pthread_key_create中定義的destructor函數。線程數據的釋放必須在釋放鍵之前完成。

4.2 互斥鎖
  互斥鎖用來保證一段時間內只有一個線程在執行一段代碼。必要性顯而易見:假設各個線程向同一個文件順序寫入數據,最後得到的結果一定是災難性的。
  我們先看下面一段代碼。這是一個讀/寫程序,它們公用一個緩衝區,並且我們假定一個緩衝區只能保存一條信息。即緩衝區只有兩個狀態:有信息或沒有信息。

void reader_function ( void );
void writer_function ( void );

char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;
void main ( void ){
pthread_t reader;
/* 定義延遲時間*/
delay.tv_sec = 2;
delay.tv_nec = 0;
/* 用默認屬性初始化一個互斥鎖對象*/
pthread_mutex_init (&mutex,NULL);
pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
writer_function( );
}

void writer_function (void){
while(1){
/* 鎖定互斥鎖*/
pthread_mutex_lock (&mutex);
if (buffer_has_item==0){
buffer=make_new_item( );
buffer_has_item=1;
}
/* 打開互斥鎖*/
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}

void reader_function(void){
while(1){
pthread_mutex_lock(&mutex);
if(buffer_has_item==1){
consume_item(buffer);
buffer_has_item=0;
}
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
  這裏聲明瞭互斥鎖變量mutex,結構pthread_mutex_t爲不公開的數據類型,其中包含一個系統分配的屬性對象。函數pthread_mutex_init用來生成一個互斥鎖。NULL參數表明使用默認屬性。如果需要聲明特定屬性的互斥鎖,須調用函數pthread_mutexattr_init。函數pthread_mutexattr_setpshared和函數pthread_mutexattr_settype用來設置互斥鎖屬性。前一個函數設置屬性pshared,它有兩個取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用來不同進程中的線程同步,後者用於同步本進程的不同線程。在上面的例子中,我們使用的是默認屬性PTHREAD_PROCESS_ PRIVATE。後者用來設置互斥鎖類型,可選的類型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它們分別定義了不同的上所、解鎖機制,一般情況下,選用最後一個默認屬性。
  pthread_mutex_lock聲明開始用互斥鎖上鎖,此後的代碼直至調用pthread_mutex_unlock爲止,均被上鎖,即同一時間只能被一個線程調用執行。當一個線程執行到pthread_mutex_lock處時,如果該鎖此時被另一個線程使用,那此線程被阻塞,即程序將等待到另一個線程釋放此互斥鎖。在上面的例子中,我們使用了pthread_delay_np函數,讓線程睡眠一段時間,就是爲了防止一個線程始終佔據此函數。
  上面的例子非常簡單,就不再介紹了,需要提出的是在使用互斥鎖的過程中很有可能會出現死鎖:兩個線程試圖同時佔用兩個資源,並按不同的次序鎖定相應的互斥鎖,例如兩個線程都需要鎖定互斥鎖1和互斥鎖2,a線程先鎖定互斥鎖1,b線程先鎖定互斥鎖2,這時就出現了死鎖。此時我們可以使用函數pthread_mutex_trylock,它是函數pthread_mutex_lock的非阻塞版本,當它發現死鎖不可避免時,它會返回相應的信息,程序員可以針對死鎖做出相應的處理。另外不同的互斥鎖類型對死鎖的處理不一樣,但最主要的還是要程序員自己在程序設計注意這一點。

4.3 條件變量
  前一節中我們講述瞭如何使用互斥鎖來實現線程間數據的共享和通信,互斥鎖一個明顯的缺點是它只有兩種狀態:鎖定和非鎖定。而條件變量通過允許線程阻塞和等待另一個線程發送信號的方法彌補了互斥鎖的不足,它常和互斥鎖一起使用。使用時,條件變量被用來阻塞一個線程,當條件不滿足時,線程往往解開相應的互斥鎖並等待條件發生變化。一旦其它的某個線程改變了條件變量,它將通知相應的條件變量喚醒一個或多個正被此條件變量阻塞的線程。這些線程將重新鎖定互斥鎖並重新測試條件是否滿足。一般說來,條件變量被用來進行線承間的同步。
  條件變量的結構爲pthread_cond_t,函數pthread_cond_init()被用來初始化一個條件變量。它的原型爲:
  extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
  其中cond是一個指向結構pthread_cond_t的指針,cond_attr是一個指向結構pthread_condattr_t的指針。結構pthread_condattr_t是條件變量的屬性結構,和互斥鎖一樣我們可以用它來設置條件變量是進程內可用還是進程間可用,默認值是PTHREAD_ PROCESS_PRIVATE,即此條件變量被同一進程內的各個線程使用。注意初始化條件變量只有未被使用時才能重新初始化或被釋放。釋放一個條件變量的函數爲pthread_cond_ destroy(pthread_cond_t cond)。 
  函數pthread_cond_wait()使線程阻塞在一個條件變量上。它的函數原型爲:
  extern int pthread_cond_wait __P ((pthread_cond_t *__cond,
  pthread_mutex_t *__mutex));
  線程解開mutex指向的鎖並被條件變量cond阻塞。線程可以被函數pthread_cond_signal和函數pthread_cond_broadcast喚醒,但是要注意的是,條件變量只是起阻塞和喚醒線程的作用,具體的判斷條件還需用戶給出,例如一個變量是否爲0等等,這一點我們從後面的例子中可以看到。線程被喚醒後,它將重新檢查判斷條件是否滿足,如果還不滿足,一般說來線程應該仍阻塞在這裏,被等待被下一次喚醒。這個過程一般用while語句實現。
  另一個用來阻塞線程的函數是pthread_cond_timedwait(),它的原型爲:
  extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
  pthread_mutex_t *__mutex, __const struct timespec *__abstime));
  它比函數pthread_cond_wait()多了一個時間參數,經歷abstime段時間後,即使條件變量不滿足,阻塞也被解除。
  函數pthread_cond_signal()的原型爲:
  extern int pthread_cond_signal __P ((pthread_cond_t *__cond));
  它用來釋放被阻塞在條件變量cond上的一個線程。多個線程阻塞在此條件變量上時,哪一個線程被喚醒是由線程的調度策略所決定的。要注意的是,必須用保護條件變量的互斥鎖來保護這個函數,否則條件滿足信號又可能在測試條件和調用pthread_cond_wait函數之間被髮出,從而造成無限制的等待。下面是使用函數pthread_cond_wait()和函數pthread_cond_signal()的一個簡單的例子。

pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;
decrement_count () {
pthread_mutex_lock (&count_lock);
while(count==0)
pthread_cond_wait( &count_nonzero, &count_lock);
count=count -1;
pthread_mutex_unlock (&count_lock);
}

increment_count(){
pthread_mutex_lock(&count_lock);
if(count==0)
pthread_cond_signal(&count_nonzero);
count=count+1;
pthread_mutex_unlock(&count_lock);
}
  count值爲0時,decrement函數在pthread_cond_wait處被阻塞,並打開互斥鎖count_lock。此時,當調用到函數increment_count時,pthread_cond_signal()函數改變條件變量,告知decrement_count()停止阻塞。讀者可以試着讓兩個線程分別運行這兩個函數,看看會出現什麼樣的結果。
  函數pthread_cond_broadcast(pthread_cond_t *cond)用來喚醒所有被阻塞在條件變量cond上的線程。這些線程被喚醒後將再次競爭相應的互斥鎖,所以必須小心使用這個函數。

4.4 信號量
  信號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。當公共資源增加時,調用函數sem_post()增加信號量。只有當信號量值大於0時,才能使用公共資源,使用後,函數sem_wait()減少信號量。函數sem_trywait()和函數pthread_ mutex_trylock()起同樣的作用,它是函數sem_wait()的非阻塞版本。下面我們逐個介紹和信號量有關的一些函數,它們都在頭文件/usr/include/semaphore.h中定義。
  信號量的數據類型爲結構sem_t,它本質上是一個長整型的數。函數sem_init()用來初始化一個信號量。它的原型爲:
  extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
  sem爲指向信號量結構的一個指針;pshared不爲0時此信號量在進程間共享,否則只能爲當前進程的所有線程共享;value給出了信號量的初始值。
  函數sem_post( sem_t *sem )用來增加信號量的值。當有線程阻塞在這個信號量上時,調用這個函數會使其中的一個線程不在阻塞,選擇機制同樣是由線程的調度策略決定的。
  函數sem_wait( sem_t *sem )被用來阻塞當前線程直到信號量sem的值大於0,解除阻塞後將sem的值減一,表明公共資源經使用後減少。函數sem_trywait ( sem_t *sem )是函數sem_wait()的非阻塞版本,它直接將信號量sem的值減一。
  函數sem_destroy(sem_t *sem)用來釋放信號量sem。
  下面我們來看一個使用信號量的例子。在這個例子中,一共有4個線程,其中兩個線程負責從文件讀取數據到公共的緩衝區,另兩個線程從緩衝區讀取數據作不同的處理(加和乘運算)。
/* File sem.c */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;
/* 從文件1.dat讀取數據,每讀一次,信號量加一*/
void ReadData1(void){
FILE *fp=fopen("1.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*從文件2.dat讀取數據*/
void ReadData2(void){
FILE *fp=fopen("2.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*阻塞等待緩衝區有數據,讀取數據後,釋放空間,繼續等待*/
void HandleData1(void){
while(1){
sem_wait(&sem);
printf("Plus:%d+%d=%d/n",stack[size][0],stack[size][1],
stack[size][0]+stack[size][1]);
--size;
}
}

void HandleData2(void){
while(1){
sem_wait(&sem);
printf("Multiply:%d*%d=%d/n",stack[size][0],stack[size][1],
stack[size][0]*stack[size][1]);
--size;
}
}
int main(void){
pthread_t t1,t2,t3,t4;
sem_init(&sem,0,0);
pthread_create(&t1,NULL,(void *)HandleData1,NULL);
pthread_create(&t2,NULL,(void *)HandleData2,NULL);
pthread_create(&t3,NULL,(void *)ReadData1,NULL);
pthread_create(&t4,NULL,(void *)ReadData2,NULL);
/* 防止程序過早退出,讓它在此無限期等待*/
pthread_join(t1,NULL);
}

  在Linux下,我們用命令gcc -lpthread sem.c -o sem生成可執行文件sem。 我們事先編輯好數據文件1.dat和2.dat,假設它們的內容分別爲1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我們運行sem,得到如下的結果:
Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11

  從中我們可以看出各個線程間的競爭關係。而數值並未按我們原先的順序顯示出來這是由於size這個數值被各個線程任意修改的緣故。這也往往是多線程編程要注意的問題。

5 小結
  多線程編程是一個很有意思也很有用的技術,使用多線程技術的網絡螞蟻是目前最常用的下載工具之一,使用多線程技術的grep比單線程的grep要快上幾倍,類似的例子還有很多。希望大家能用多線程技術寫出高效實用的好程序來。

 

 

二、Linux 線程實現機制分析

 

自從多線程編程的概念出現在 Linux 中以來,Linux 多線應用的發展總是與兩個問題脫不開干係:兼容性、效率。本文從線程模型入手,通過分析目前 Linux 平臺上最流行的 LinuxThreads 線程庫的實現及其不足,描述了 Linux 社區是如何看待和解決兼容性和效率這兩個問題的。
一.基礎知識:線程和進程

按照教科書上的定義,進程是資源管理的最小單位,線程是程序執行的最小單位。在操作系統設計上,從進程演化出線程,最主要的目的就是更好的支持SMP以及減小(進程/線程)上下文切換開銷。

無論按照怎樣的分法,一個進程至少需要一個線程作爲它的指令執行體,進程管理着資源(比如cpu、內存、文件等等),而將線程分配到某個cpu上執行。一個進程當然可以擁有多個線程,此時,如果進程運行在SMP機器上,它就可以同時使用多個cpu來執行各個線程,達到最大程度的並行,以提高效率;同時,即使是在單cpu的機器上,採用多線程模型來設計程序,正如當年採用多進程模型代替單進程模型一樣,使設計更簡潔、功能更完備,程序的執行效率也更高,例如採用多個線程響應多個輸入,而此時多線程模型所實現的功能實際上也可以用多進程模型來實現,而與後者相比,線程的上下文切換開銷就比進程要小多了,從語義上來說,同時響應多個輸入這樣的功能,實際上就是共享了除cpu以外的所有資源的。

針對線程模型的兩大意義,分別開發出了核心級線程和用戶級線程兩種線程模型,分類的標準主要是線程的調度者在覈內還是在覈外。前者更利於併發使用多處理器的資源,而後者則更多考慮的是上下文切換開銷。在目前的商用系統中,通常都將兩者結合起來使用,既提供核心線程以滿足smp系統的需要,也支持用線程庫的方式在用戶態實現另一套線程機制,此時一個核心線程同時成爲多個用戶態線程的調度者。正如很多技術一樣,"混合"通常都能帶來更高的效率,但同時也帶來更大的實現難度,出於"簡單"的設計思路,Linux從一開始就沒有實現混合模型的計劃,但它在實現上採用了另一種思路的"混合"。

在線程機制的具體實現上,可以在操作系統內核上實現線程,也可以在覈外實現,後者顯然要求核內至少實現了進程,而前者則一般要求在覈內同時也支持進程。核心級線程模型顯然要求前者的支持,而用戶級線程模型則不一定基於後者實現。這種差異,正如前所述,是兩種分類方式的標準不同帶來的。

當核內既支持進程也支持線程時,就可以實現線程-進程的"多對多"模型,即一個進程的某個線程由核內調度,而同時它也可以作爲用戶級線程池的調度者,選擇合適的用戶級線程在其空間中運行。這就是前面提到的"混合"線程模型,既可滿足多處理機系統的需要,也可以最大限度的減小調度開銷。絕大多數商業操作系統(如Digital Unix、Solaris、Irix)都採用的這種能夠完全實現POSIX1003.1c標準的線程模型。在覈外實現的線程又可以分爲"一對一"、"多對一"兩種模型,前者用一個核心進程(也許是輕量進程)對應一個線程,將線程調度等同於進程調度,交給核心完成,而後者則完全在覈外實現多線程,調度也在用戶態完成。後者就是前面提到的單純的用戶級線程模型的實現方式,顯然,這種核外的線程調度器實際上只需要完成線程運行棧的切換,調度開銷非常小,但同時因爲核心信號(無論是同步的還是異步的)都是以進程爲單位的,因而無法定位到線程,所以這種實現方式不能用於多處理器系統,而這個需求正變得越來越大,因此,在現實中,純用戶級線程的實現,除算法研究目的以外,幾乎已經消失了。

Linux內核只提供了輕量進程的支持,限制了更高效的線程模型的實現,但Linux着重優化了進程的調度開銷,一定程度上也彌補了這一缺陷。目前最流行的線程機制LinuxThreads所採用的就是線程-進程"一對一"模型,調度交給核心,而在用戶級實現一個包括信號處理在內的線程管理機制。Linux-LinuxThreads的運行機制正是本文的描述重點。

 

 


 回頁首
 

 


二.Linux 2.4內核中的輕量進程實現

最初的進程定義都包含程序、資源及其執行三部分,其中程序通常指代碼,資源在操作系統層面上通常包括內存資源、IO資源、信號處理等部分,而程序的執行通常理解爲執行上下文,包括對cpu的佔用,後來發展爲線程。在線程概念出現以前,爲了減小進程切換的開銷,操作系統設計者逐漸修正進程的概念,逐漸允許將進程所佔有的資源從其主體剝離出來,允許某些進程共享一部分資源,例如文件、信號,數據內存,甚至代碼,這就發展出輕量進程的概念。Linux內核在2.0.x版本就已經實現了輕量進程,應用程序可以通過一個統一的clone()系統調用接口,用不同的參數指定創建輕量進程還是普通進程。在內核中,clone()調用經過參數傳遞和解釋後會調用do_fork(),這個核內函數同時也是fork()、vfork()系統調用的最終實現:

<linux-2.4.20/kernel/fork.c>
int do_fork(unsigned long clone_flags, unsigned long stack_start,
struct pt_regs *regs, unsigned long stack_size)
 

 

其中的clone_flags取自以下宏的"或"值:

<linux-2.4.20/include/linux/sched.h>
#define CSIGNAL   0x000000ff /* signal mask to be sent at exit */
#define CLONE_VM  0x00000100 /* set if VM shared between processes */
#define CLONE_FS        0x00000200 /* set if fs info shared between processes */
#define CLONE_FILES     0x00000400 /* set if open files shared between processes */
#define CLONE_SIGHAND 0x00000800 /* set if signal handlers and blocked signals shared */
#define CLONE_PID  0x00001000 /* set if pid shared */
#define CLONE_PTRACE 0x00002000 /* set if we want to let tracing continue on the child too */
#define CLONE_VFORK 0x00004000 /* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENT 0x00008000 /* set if we want to have the same parent as the cloner */
#define CLONE_THREAD 0x00010000 /* Same thread group? */
#define CLONE_NEWNS 0x00020000 /* New namespace group? */
#define CLONE_SIGNAL  (CLONE_SIGHAND | CLONE_THREAD)
 

 

在do_fork()中,不同的clone_flags將導致不同的行爲,對於LinuxThreads,它使用(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND)參數來調用clone()創建"線程",表示共享內存、共享文件系統訪問計數、共享文件描述符表,以及共享信號處理方式。本節就針對這幾個參數,看看Linux內核是如何實現這些資源的共享的。

1.CLONE_VM

do_fork()需要調用copy_mm()來設置task_struct中的mm和active_mm項,這兩個mm_struct數據與進程所關聯的內存空間相對應。如果do_fork()時指定了CLONE_VM開關,copy_mm()將把新的task_struct中的mm和active_mm設置成與current的相同,同時提高該mm_struct的使用者數目(mm_struct::mm_users)。也就是說,輕量級進程與父進程共享內存地址空間,由下圖示意可以看出mm_struct在進程中的地位:


 


2.CLONE_FS

task_struct中利用fs(struct fs_struct *)記錄了進程所在文件系統的根目錄和當前目錄信息,do_fork()時調用copy_fs()複製了這個結構;而對於輕量級進程則僅增加fs->count計數,與父進程共享相同的fs_struct。也就是說,輕量級進程沒有獨立的文件系統相關的信息,進程中任何一個線程改變當前目錄、根目錄等信息都將直接影響到其他線程。

3.CLONE_FILES

一個進程可能打開了一些文件,在進程結構task_struct中利用files(struct files_struct *)來保存進程打開的文件結構(struct file)信息,do_fork()中調用了copy_files()來處理這個進程屬性;輕量級進程與父進程是共享該結構的,copy_files()時僅增加files->count計數。這一共享使得任何線程都能訪問進程所維護的打開文件,對它們的操作會直接反映到進程中的其他線程。

4.CLONE_SIGHAND

每一個Linux進程都可以自行定義對信號的處理方式,在task_struct中的sig(struct signal_struct)中使用一個struct k_sigaction結構的數組來保存這個配置信息,do_fork()中的copy_sighand()負責複製該信息;輕量級進程不進行復制,而僅僅增加signal_struct::count計數,與父進程共享該結構。也就是說,子進程與父進程的信號處理方式完全相同,而且可以相互更改。

do_fork()中所做的工作很多,在此不詳細描述。對於SMP系統,所有的進程fork出來後,都被分配到與父進程相同的cpu上,一直到該進程被調度時纔會進行cpu選擇。

儘管Linux支持輕量級進程,但並不能說它就支持核心級線程,因爲Linux的"線程"和"進程"實際上處於一個調度層次,共享一個進程標識符空間,這種限制使得不可能在Linux上實現完全意義上的POSIX線程機制,因此衆多的Linux線程庫實現嘗試都只能儘可能實現POSIX的絕大部分語義,並在功能上儘可能逼近。

 

 


 回頁首
 

 


三.LinuxThread的線程機制

LinuxThreads是目前Linux平臺上使用最爲廣泛的線程庫,由Xavier Leroy ([email protected])負責開發完成,並已綁定在GLIBC中發行。它所實現的就是基於核心輕量級進程的"一對一"線程模型,一個線程實體對應一個核心輕量級進程,而線程之間的管理在覈外函數庫中實現。

1.線程描述數據結構及實現限制

LinuxThreads定義了一個struct _pthread_descr_struct數據結構來描述線程,並使用全局數組變量__pthread_handles來描述和引用進程所轄線程。在__pthread_handles中的前兩項,LinuxThreads定義了兩個全局的系統線程:__pthread_initial_thread和__pthread_manager_thread,並用__pthread_main_thread表徵__pthread_manager_thread的父線程(初始爲__pthread_initial_thread)。

struct _pthread_descr_struct是一個雙環鏈表結構,__pthread_manager_thread所在的鏈表僅包括它一個元素,實際上,__pthread_manager_thread是一個特殊線程,LinuxThreads僅使用了其中的errno、p_pid、p_priority等三個域。而__pthread_main_thread所在的鏈則將進程中所有用戶線程串在了一起。經過一系列pthread_create()之後形成的__pthread_handles數組將如下圖所示:


 


新創建的線程將首先在__pthread_handles數組中佔據一項,然後通過數據結構中的鏈指針連入以__pthread_main_thread爲首指針的鏈表中。這個鏈表的使用在介紹線程的創建和釋放的時候將提到。

LinuxThreads遵循POSIX1003.1c標準,其中對線程庫的實現進行了一些範圍限制,比如進程最大線程數,線程私有數據區大小等等。在LinuxThreads的實現中,基本遵循這些限制,但也進行了一定的改動,改動的趨勢是放鬆或者說擴大這些限制,使編程更加方便。這些限定宏主要集中在sysdeps/unix/sysv/linux/bits/local_lim.h(不同平臺使用的文件位置不同)中,包括如下幾個:

每進程的私有數據key數,POSIX定義_POSIX_THREAD_KEYS_MAX爲128,LinuxThreads使用PTHREAD_KEYS_MAX,1024;私有數據釋放時允許執行的操作數,LinuxThreads與POSIX一致,定義PTHREAD_DESTRUCTOR_ITERATIONS爲4;每進程的線程數,POSIX定義爲64,LinuxThreads增大到1024(PTHREAD_THREADS_MAX);線程運行棧最小空間大小,POSIX未指定,LinuxThreads使用PTHREAD_STACK_MIN,16384(字節)。

2.管理線程

"一對一"模型的好處之一是線程的調度由核心完成了,而其他諸如線程取消、線程間的同步等工作,都是在覈外線程庫中完成的。在LinuxThreads中,專門爲每一個進程構造了一個管理線程,負責處理線程相關的管理工作。當進程第一次調用pthread_create()創建一個線程的時候就會創建(__clone())並啓動管理線程。

在一個進程空間內,管理線程與其他線程之間通過一對"管理管道(manager_pipe[2])"來通訊,該管道在創建管理線程之前創建,在成功啓動了管理線程之後,管理管道的讀端和寫端分別賦給兩個全局變量__pthread_manager_reader和__pthread_manager_request,之後,每個用戶線程都通過__pthread_manager_request向管理線程發請求,但管理線程本身並沒有直接使用__pthread_manager_reader,管道的讀端(manager_pipe[0])是作爲__clone()的參數之一傳給管理線程的,管理線程的工作主要就是監聽管道讀端,並對從中取出的請求作出反應。

創建管理線程的流程如下所示:
(全局變量pthread_manager_request初值爲-1)


 


初始化結束後,在__pthread_manager_thread中記錄了輕量級進程號以及核外分配和管理的線程id,2*PTHREAD_THREADS_MAX+1這個數值不會與任何常規用戶線程id衝突。管理線程作爲pthread_create()的調用者線程的子線程運行,而pthread_create()所創建的那個用戶線程則是由管理線程來調用clone()創建,因此實際上是管理線程的子線程。(此處子線程的概念應該當作子進程來理解。)

__pthread_manager()就是管理線程的主循環所在,在進行一系列初始化工作後,進入while(1)循環。在循環中,線程以2秒爲timeout查詢(__poll())管理管道的讀端。在處理請求前,檢查其父線程(也就是創建manager的主線程)是否已退出,如果已退出就退出整個進程。如果有退出的子線程需要清理,則調用pthread_reap_children()清理。

然後纔是讀取管道中的請求,根據請求類型執行相應操作(switch-case)。具體的請求處理,源碼中比較清楚,這裏就不贅述了。

3.線程棧

在LinuxThreads中,管理線程的棧和用戶線程的棧是分離的,管理線程在進程堆中通過malloc()分配一個THREAD_MANAGER_STACK_SIZE字節的區域作爲自己的運行棧。

用戶線程的棧分配辦法隨着體系結構的不同而不同,主要根據兩個宏定義來區分,一個是NEED_SEPARATE_REGISTER_STACK,這個屬性僅在IA64平臺上使用;另一個是FLOATING_STACK宏,在i386等少數平臺上使用,此時用戶線程棧由系統決定具體位置並提供保護。與此同時,用戶還可以通過線程屬性結構來指定使用用戶自定義的棧。因篇幅所限,這裏只能分析i386平臺所使用的兩種棧組織方式:FLOATING_STACK方式和用戶自定義方式。

在FLOATING_STACK方式下,LinuxThreads利用mmap()從內核空間中分配8MB空間(i386系統缺省的最大棧空間大小,如果有運行限制(rlimit),則按照運行限制設置),使用mprotect()設置其中第一頁爲非訪問區。該8M空間的功能分配如下圖:


 


低地址被保護的頁面用來監測棧溢出。

對於用戶指定的棧,在按照指針對界後,設置線程棧頂,並計算出棧底,不做保護,正確性由用戶自己保證。

不論哪種組織方式,線程描述結構總是位於棧頂緊鄰堆棧的位置。

4.線程id和進程id

每個LinuxThreads線程都同時具有線程id和進程id,其中進程id就是內核所維護的進程號,而線程id則由LinuxThreads分配和維護。

__pthread_initial_thread的線程id爲PTHREAD_THREADS_MAX,__pthread_manager_thread的是2*PTHREAD_THREADS_MAX+1,第一個用戶線程的線程id爲PTHREAD_THREADS_MAX+2,此後第n個用戶線程的線程id遵循以下公式:

 tid=n*PTHREAD_THREADS_MAX+n+1
 

 

這種分配方式保證了進程中所有的線程(包括已經退出)都不會有相同的線程id,而線程id的類型pthread_t定義爲無符號長整型(unsigned long int),也保證了有理由的運行時間內線程id不會重複。

從線程id查找線程數據結構是在pthread_handle()函數中完成的,實際上只是將線程號按PTHREAD_THREADS_MAX取模,得到的就是該線程在__pthread_handles中的索引。

5.線程的創建

在pthread_create()向管理線程發送REQ_CREATE請求之後,管理線程即調用pthread_handle_create()創建新線程。分配棧、設置thread屬性後,以pthread_start_thread()爲函數入口調用__clone()創建並啓動新線程。pthread_start_thread()讀取自身的進程id號存入線程描述結構中,並根據其中記錄的調度方法配置調度。一切準備就緒後,再調用真正的線程執行函數,並在此函數返回後調用pthread_exit()清理現場。

6.LinuxThreads的不足

由於Linux內核的限制以及實現難度等等原因,LinuxThreads並不是完全POSIX兼容的,在它的發行README中有說明。

1)進程id問題

這個不足是最關鍵的不足,引起的原因牽涉到LinuxThreads的"一對一"模型。

Linux內核並不支持真正意義上的線程,LinuxThreads是用與普通進程具有同樣內核調度視圖的輕量級進程來實現線程支持的。這些輕量級進程擁有獨立的進程id,在進程調度、信號處理、IO等方面享有與普通進程一樣的能力。在源碼閱讀者看來,就是Linux內核的clone()沒有實現對CLONE_PID參數的支持。

在內核do_fork()中對CLONE_PID的處理是這樣的:

          if (clone_flags & CLONE_PID) {
                if (current->pid)
                        goto fork_out;
        }
        

 

這段代碼表明,目前的Linux內核僅在pid爲0的時候認可CLONE_PID參數,實際上,僅在SMP初始化,手工創建進程的時候纔會使用CLONE_PID參數。

按照POSIX定義,同一進程的所有線程應該共享一個進程id和父進程id,這在目前的"一對一"模型下是無法實現的。

2)信號處理問題

由於異步信號是內核以進程爲單位分發的,而LinuxThreads的每個線程對內核來說都是一個進程,且沒有實現"線程組",因此,某些語義不符合POSIX標準,比如沒有實現向進程中所有線程發送信號,README對此作了說明。

如果核心不提供實時信號,LinuxThreads將使用SIGUSR1和SIGUSR2作爲內部使用的restart和cancel信號,這樣應用程序就不能使用這兩個原本爲用戶保留的信號了。在Linux kernel 2.1.60以後的版本都支持擴展的實時信號(從_SIGRTMIN到_SIGRTMAX),因此不存在這個問題。

某些信號的缺省動作難以在現行體系上實現,比如SIGSTOP和SIGCONT,LinuxThreads只能將一個線程掛起,而無法掛起整個進程。

3)線程總數問題

LinuxThreads將每個進程的線程最大數目定義爲1024,但實際上這個數值還受到整個系統的總進程數限制,這又是由於線程其實是核心進程。

在kernel 2.4.x中,採用一套全新的總進程數計算方法,使得總進程數基本上僅受限於物理內存的大小,計算公式在kernel/fork.c的fork_init()函數中:

 max_threads = mempages / (THREAD_SIZE/PAGE_SIZE) / 8
 

 

在i386上,THREAD_SIZE=2*PAGE_SIZE,PAGE_SIZE=2^12(4KB),mempages=物理內存大小/PAGE_SIZE,對於256M的內存的機器,mempages=256*2^20/2^12=256*2^8,此時最大線程數爲4096。

但爲了保證每個用戶(除了root)的進程總數不至於佔用一半以上物理內存,fork_init()中繼續指定:

    init_task.rlim[RLIMIT_NPROC].rlim_cur = max_threads/2;
    init_task.rlim[RLIMIT_NPROC].rlim_max = max_threads/2;
    

 

這些進程數目的檢查都在do_fork()中進行,因此,對於LinuxThreads來說,線程總數同時受這三個因素的限制。

4)管理線程問題

管理線程容易成爲瓶頸,這是這種結構的通病;同時,管理線程又負責用戶線程的清理工作,因此,儘管管理線程已經屏蔽了大部分的信號,但一旦管理線程死亡,用戶線程就不得不手工清理了,而且用戶線程並不知道管理線程的狀態,之後的線程創建等請求將無人處理。

5)同步問題

LinuxThreads中的線程同步很大程度上是建立在信號基礎上的,這種通過內核複雜的信號處理機制的同步方式,效率一直是個問題。

6)其他POSIX兼容性問題

Linux中很多系統調用,按照語義都是與進程相關的,比如nice、setuid、setrlimit等,在目前的LinuxThreads中,這些調用都僅僅影響調用者線程。

7)實時性問題

線程的引入有一定的實時性考慮,但LinuxThreads暫時不支持,比如調度選項,目前還沒有實現。不僅LinuxThreads如此,標準的Linux在實時性上考慮都很少。

 

 


 回頁首
 

 


四.其他的線程實現機制

LinuxThreads的問題,特別是兼容性上的問題,嚴重阻礙了Linux上的跨平臺應用(如Apache)採用多線程設計,從而使得Linux上的線程應用一直保持在比較低的水平。在Linux社區中,已經有很多人在爲改進線程性能而努力,其中既包括用戶級線程庫,也包括核心級和用戶級配合改進的線程庫。目前最爲人看好的有兩個項目,一個是RedHat公司牽頭研發的NPTL(Native Posix Thread Library),另一個則是IBM投資開發的NGPT(Next Generation Posix Threading),二者都是圍繞完全兼容POSIX 1003.1c,同時在覈內和核外做工作以而實現多對多線程模型。這兩種模型都在一定程度上彌補了LinuxThreads的缺點,且都是重起爐竈全新設計的。

1.NPTL

NPTL的設計目標歸納可歸納爲以下幾點:

POSIX兼容性
SMP結構的利用
低啓動開銷
低鏈接開銷(即不使用線程的程序不應當受線程庫的影響)
與LinuxThreads應用的二進制兼容性
軟硬件的可擴展能力
多體系結構支持
NUMA支持
與C++集成
在技術實現上,NPTL仍然採用1:1的線程模型,並配合glibc和最新的Linux Kernel2.5.x開發版在信號處理、線程同步、存儲管理等多方面進行了優化。和LinuxThreads不同,NPTL沒有使用管理線程,核心線程的管理直接放在覈內進行,這也帶了性能的優化。

主要是因爲核心的問題,NPTL仍然不是100%POSIX兼容的,但就性能而言相對LinuxThreads已經有很大程度上的改進了。

2.NGPT

IBM的開放源碼項目NGPT在2003年1月10日推出了穩定的2.2.0版,但相關的文檔工作還差很多。就目前所知,NGPT是基於GNU Pth(GNU Portable Threads)項目而實現的M:N模型,而GNU Pth是一個經典的用戶級線程庫實現。

按照2003年3月NGPT官方網站上的通知,NGPT考慮到NPTL日益廣泛地爲人所接受,爲避免不同的線程庫版本引起的混亂,今後將不再進行進一步開發,而今進行支持性的維護工作。也就是說,NGPT已經放棄與NPTL競爭下一代Linux POSIX線程庫標準。

3.其他高效線程機制

此處不能不提到Scheduler Activations。這個1991年在ACM上發表的多線程內核結構影響了很多多線程內核的設計,其中包括Mach3.0、NetBSD和商業版本Digital Unix(現在叫Compaq True64 Unix)。它的實質是在使用用戶級線程調度的同時,儘可能地減少用戶級對核心的系統調用請求,而後者往往是運行開銷的重要來源。採用這種結構的線程機制,實際上是結合了用戶級線程的靈活高效和核心級線程的實用性,因此,包括Linux、FreeBSD在內的多個開放源碼操作系統設計社區都在進行相關研究,力圖在本系統中實現Scheduler Activations。

 

 

參考資料

[Linus Torvalds,2002] Linux內核源碼v2.4.20

[GNU,2002] Glibc源碼v2.2.2(內含LinuxThreads v0.9)

[Thomas E. Terrill,1997] An Introduction to Threads Using The LinuxThreads Interface

[Ulrich Drepper,Ingo Molnar,2003] The Native POSIX Thread Library for Linux
http://www.ibm.com/developerworks/oss/pthreads/,NGPT官方網站
[Ralf S. Engelschall,2000] Portable Multithreading

[Thomas E. Anderson, Brian N. Bershad, Edward D. Lazowska, Henry M. Levy,1992] Scheduler Activations: Effective Kernel Support for the User-Level Management of Parallelism

[[email protected]] Linux線程初探

三、unix線程和windows線程的區別

 

這裏先介紹下線程的概念,線程(thread)技術早在60年代就被提出,但真正應用多線程到操作系統中去,是在80年代中期,Sun的Solaris是這方面的佼佼者。傳統的Unix也支持線程的概念,但是在一個進程(process)中只允許有一個線程,這樣多線程就意味着多進程,不能像Windows NT系統那樣(2000是NT5.0,XP是NT5.1,2K3是NT5.2)一個進程有很多個線程。現在,多線程技術已經被許多操作系統所支持,包括Windows/NT和Linux。但是線程的實現在類Unix下並不相同,基本上分爲內核支持方式和用戶空間支持方式,如果線程的上下文切換是在內核中實現的,我們就稱之爲內核方式實現,但如果線程的切換是在用戶空間進行的我們就稱之爲用戶方式實現,內核並不知情,當然還有兩種方式的混合方式,用戶空間中的多個線程在內核空間有相應的內核線程與之對應(通常我們稱此內核線程爲LWP-輕級進程)。
我們再看看linux下線程的實現,linux的線程編程有兩個庫pthread和pth,對於pthread的實現是內核方式的實現,每個線程在kernel中都有task結構與之對應,也就是說用ps命令行是可以看見多個線程,線程的調度也是由內核中的schedule進行的。
再來看看Windows的多線程,Windows NT和Windows95是一個搶先型多任務、多線程操作系統。因爲它使用搶先型的多任務,所以它擁有與UNIX同樣平滑的處理和進程獨立。多線程就更進一步。一個獨立的程序默認是使用一個線程,不過它可以將自己分解爲幾個獨立的線程來執行,例如,其中的一個線程可以發送一個文件到打印機,而另一個可以響應用戶的輸入。這個簡單的程序設計修改可以明顯減少用戶等待的時間,讓用戶無需擔心長時間的計算、重繪屏幕、文件讀寫等帶來的不便。
多線程還可以讓你從許多高端的多處理器NT機器中得到好處。例如,你購買了一個高級的RISC機器,可以使用多達10個CPU芯片,但在開始的時候你只購買了一個CPU。你寫了一個簡單的Mandelbrot set程序,你發現需要15秒的時間來重新繪製Mandelbrot set的畫面。

那麼,Windows平臺的線程和類Unix平臺(包括Linux)的進程的區別是什麼呢?
  熟悉WIN32編程的人一定知道,WIN32的進程管理方式與UNIX上有着很大區別,在UNIX裏,只有進程的概念,但在WIN32裏卻還有一個“線程”的概念,那麼UNIX和WIN32在這裏究竟有着什麼區別呢?
  UNIX裏的fork是七十年代UNIX早期的開發者經過長期在理論和實踐上的艱苦探索後取得的成果,一方面,它使操作系統在進程管理上付出了最小的代價,另一方面,又爲程序員提供了一個簡潔明瞭的多進程方法。
  WIN32裏的進程/線程是繼承自OS/2的。在WIN32裏,“進程”是指一個程序,而“線程”是一個“進程”裏的一個執行“線索”。從核心上講,WIN32的多進程與UNIX並無多大的區別,在WIN32裏的線程才相當於UNIX的進程,是一個實際正在執行的代碼。但是,WIN32裏同一個進程裏各個線程之間是共享數據段的。這纔是與UNIX的進程最大的不同。
對於多任務系統,共享數據區是必要的,但也是一個容易引起混亂的問題,在WIN32下,一個程序員很容易忘記線程之間的數據是共享的這一情況,一個線程修改過一個變量後,另一個線程卻又修改了它,結果引起程序出問題。但在UNIX下,由於變量本來並不共享,而由程序員來顯式地指定要共享的數據,使程序變得更清晰與安全。
至於WIN32的“進程”概念,其含義則是“應用程序”,也就是相當於UNIX下的exec了。

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