線程擁有寄存器,用來保存當前的工作變量;線程有自己的棧堆,用來保存上下文,在同一個進程當中,允許擁有較大獨立性多個線程,是對一個計算機上多個進程的模擬,在單核CPU中,每個線程分配的CPU速度的V/N。
線程實現方式
1. 用戶級線程(多對一):
把線程表放在用戶空間中,切換快。在用戶空間中可以防止某些垃圾回收程序過早退出。
缺點:無法在系統級上實現調度,無法方便地實現阻塞型系統調用(因為如果當一個線程發起阻塞型調用而阻塞時,其他線程必定會阻塞,內核並不能看到用戶線程,也就無法調度)。
->用戶級線程的調度程序激活機制:
內核給每一個進程安排一定數量的虛擬處理器,讓用戶空間線程分配到這些處理器上,當一個線程被阻塞時,則內核通知該線程的運行時系統,並且在堆棧上以參數的形式傳遞有問題的線程編號和所發生的時間的描述(這種方法被稱為是上型調用upcall),交給運行時系統自行判斷。
如果發生系統調用,同樣會引發上行調用,此時運行時系統決定調度哪個線程:被中斷的線程,新的就緒線程或者還是第三個線程。
2. 內核級線程(一對一):
把線程表放在內核空間中,每撤銷一個線程即執行一次系統調用。
缺點:開銷很大。
3. 混合實現(多對多):
采用這種方法,內核只是別內核級線程並且對其進行調度,其中一些內核及縣城會被多個用戶級線程多路復用。而每個內核級線程都有一個可以輪流使用的用戶級線程的集合。
缺點:難以實現。
Linux線程實現例子:LiunxThreads與NPTL(Native POSIX Threads Library)
一. LinuxThreads的線程機制
LinuxThreads是目前Linux平台上使用最為廣泛的線程庫,由Xavier Leroy (Xavier.Leroy@inria.fr)負責開發完成,並已綁定在GLIBC中發行。它所實現的就是基於核心輕量級進程的"一對一"線程模型,一個線程實體對應一個核心輕量級進程,而線程之間的管理在核外函數庫中實現。(LinuxThreads的詳細介紹https://www.ibm.com/developerworks/cn/linux/kernel/l-thread/。)
大體上說,LinuxThreads用一個數據結構體來描述線程,並且定義了兩個全局系統線程__pthread_initial_thread和__pthread_manager_thread,並用__pthread_main_thread表征__pthread_manager_thread的父線程(初始為__pthread_initial_thread)。
__pthread_manager_thread顧名思義是來管理線程的,而且LinuxsThreads為每一個進程都構造了管理線程。管理線程與其他線程之間用管道進行通訊。在LinuxThreads中,管理線程的棧和用戶線程的棧是分離的,管理線程在進程堆中通過malloc()分配一個THREAD_MANAGER_STACK_SIZE字節的區域作為自己的運行棧。並且每一個線程都同時具有線程ID好進程ID,其中進程ID就是內核所維護的進程號,而線程id則是由LinuxThreads分配和維護的。
LinuxThreads的不足
由於Linux內核的限制以及實現難度等等原因,LinuxThreads並不是完全POSIX兼容的,並且linux內核也不支持真正意義上的線程,LinuxThreads是用與普通進程具有同樣內核調度視圖的輕量級進程來實現線程支持的。無法實現所有線程應該共享一個進程id和父進程id。
由於異步信號是內核以進程為單位分發的,而LinuxThreads的每個線程對內核來說都是一個進程,且沒有實現"線程組",如果核心沒有提供實時信號,那么SIGUSR1和SIGUSR2這兩個信號無法使用。(在Linux kernel 2.1.60以后的版本都支持擴展的實時信號(從_SIGRTMIN到_SIGRTMAX))。某些信號的缺省動作難以在現行體系上實現,比如SIGSTOP和SIGCONT,LinuxThreads只能將一個線程掛起,而無法掛起整個進程。正因為LinuxThreads是內核基於進程的模擬,所以它的兼容性也很差,而且不支持調度。
因為LinuxThreads的管理線程方式,以及線程同步方式極大地制約了LinuxThreads的性能。管理線程這種結構本身效率不高,並且管理線程有一個巨大的缺陷:一旦管理線程死亡,線程將無法處理;在線程同步上,LinuxThreads中的線程同步基於信號,必須通過內核的復雜處理,效率一直是個問題。
二. Linux NPTL線程管理機制
上面所講到的LinuxThreads由於性能比較差,所以現在高版本的內核已經不用LinuxThreads了,現在使用的是NPTL(Native POSIX Threads Library)的方式來實現線程,NPTL是一對一的內核態線程的實現。
NPTL相比於LinuxThreads有很大的性能提升。首先NPTL 沒有使用管理線程,避免了創建管理線程所占用的線程,同時NPTL完全兼容POSIX,實現了在一個進程中所有的線程都是一樣的UID和GID(需要信號支持,並且這些信號不能用在用戶程序上,不過POSIX定義了一些包裝函數防止誤用。)
其次NPTL可以采用Futex(Fast Userspace Mutexes)來進行線程原子性同步,我們從這個東西的名字就可以知道,這種互斥鎖是定義在用戶空間的,避免了陷入內核帶來的性能損耗問題。
在http://www.akkadia.org/drepper/nptl-design.pdf這份草安裝,我們可以看到NPTL對於LinuxThreads有多大的提升



一些特別的線程
1. 彈出性線程
一個消息導致系統創建一個處理該消息的線程,這種線程被稱為是彈出式線程,彈出式線程沒有歷史,沒有必須存儲的寄存器,棧堆等東西,執行非常快。一般在內核空間中實現彈出式線程,因為此時彈出式線程可以訪問到所有的系統信息額I/O設備,在處理中斷的時候很有用。
2. 纖程
在一個搶占式系統中,普通的線程可能會被打斷,其一些信息會被保留下來(即使不是完整的),但是對於纖程,其啟動和停止都是確定的,這就意味着他不需要歷史,不需要上下文切換,避免了上下文切換帶來性能損失。纖程一般在用戶態空間實現。
3. 協程
協程當用戶態線程需要自己調用某個方法來主動地交出控制權,我們就稱為這樣的線程是協作式的,即協程。注意協程不需要操作系統陷入內核開線程,協程的實現是在用戶空間實現的,避免CPU以陷入內核的方式切換線程帶來的性能損耗問題,有效減少線程數,而且同時降低內存的使用率,提高CPU命中率,也提高了系統整體效率。
協程可以實現異步代碼(需要語言支持),比如.NET的異步編程(再也不用寫callback了)
async void func() { ... await someAsyncfunction();//此處交出控制權異步執行代碼 ...//異步執行代碼返回點(與) }
一些特別的線程線程的臨界區保護
當兩個線程要訪問同一個變量的時候,就會發生競爭,此時需要線程同步,線程同步的關鍵就是要解決臨界區的問題。
解決競爭問題,要遵循下面的原則:
任何兩個進程/線程不能同時處於臨界區
不應該對CPU的速度和數量進行假設
臨界區外執行的進程/線程不能被阻塞
不得讓進程無限制地等待進入臨界區
1. Peterson解法解決競爭問題(忙等待方式,消耗CPU資源,但是很巧)
#define FALSE 0 #define TRUE 1 #define N 2 int turn; int interested[N]; void enter_region(int process) { int other; otehr = 1 - process; interested[process] = TRUE; turn = process; while (turn == process && interested[otehr] == TRUE); } void leave_region(int process) { interested[process] = FALSE; }
當兩個進程/線程同時進入函數時,后進入的一方會覆蓋掉先進入一方的turn值,導致后進入一方被阻塞,而先進入一方則不會被阻塞而進入臨界區, 直到退出后,后進入一方便可進入臨界區了。
2. TSL指令(原子鎖,同樣也是忙等待)
enter_region: TSL REGISTER,LOCK CMP REGISTER, #0 JNE enter_region RET leave_region: MOV LOCK, #0 RET
不過intel CPU的底層同步並不用TSL指令,而是直接用的交換:
enter_region: MOV REGISTER,#1 XCHG REGISTER, LOCK CMP REGISTER, #0 JNE enter_region RET leave_region: MOV LOCK, #0 RET
3. 信號量
信號量是相當經典的線程同步的部件了,信號量有兩個操作
down操作:使信號量計數-1,當信號量為0時則阻塞當前線程,直到信號量大於0為止。
up操作:使信號量計數+1,永遠不會阻塞線程。
4. 互斥量與條件變量
互斥量是信號量的簡化版,互斥量只有上鎖和解鎖兩個狀態,互斥量可以用TSL指令實現
mutex_lock: TSL REGISTER,MUTEX ;將互斥量復制到寄存器,並且將互斥量置1 CMP REGISTER,#0 JZE ok ;如果互斥量是0,那么直接跳出(順便還鎖上了) CALL thread_yield ;否則進行調度 JMP mutex_lock ;返回調用者,進入臨界區 ok: RET mutex_lock: MOV MUTEX, #0 RET
通常互斥量和條件變量一起使用,條件變量用於綁定一個互斥量,並且當條件滿足時,發出信號,讓被鎖住的線程繼續執行(看下面生產者與消費者的例子)。
5. 管程
管程其實就是簡化了互斥量的使用的工具,需要語言支持,在Java里面使用的是synchronized的方法,而在C#中,也可以像Java一樣,使用[MethodImpl(MethodImplOptions.Synchronized)]。
6. 消息傳遞
這是Unix的經典方法了,在Unix網絡編程的卷二里面又講到,有點類似於TCP之間的傳遞信號,一個線程send,另一個receive。
7. 屏障
其實屏障我覺得不是嚴格的線程同步,比如有一個多線程的矩陣乘法操作,當屏障開啟時,只有當所有的線程/進程完成工作后,才會執行下一次工作。
經典線程同步問題
1. 生產者消費者問題
生產者消費者問題:假設有一個生產者生產產品,有一個消費者消費產品,如何做到線程同步?
其實這個問題在所有的編程書上,只要涉及到多線程都會有的題目。
用信號量解決
typedef int semaphore; semaphore mutex = 1; semaphore empty = N; semaphore full = 0; void product(void) { int item; while(1) { item = product_item(); down (&empty); down (&mutex); insert_item(item); up(&mutex); up(&full); } } void cosumer(void) { int item; while(1) { down(&full); down(&mutex); item = remove_item(); up(&mutex); up(&empty); comsume_item(item); } }
注意上鎖和解鎖的順序,一定要按照先入后出的方法,不然會造成死鎖
用條件變量和信號量解決
#define MAX 1000; pthread_mutex_t mutex; pthread_cond_t condc, condp; int buffer = 0; void *producer(void *ptr) { int i; for (i = 0; i < MAX;i++) { pthread_mutex_lock(&mutex); while(buffer != 0) pthread_cond_wait(&condp, &mutex); //此時阻塞線程 buffer = i; pthread_cond_signal(&condc); //喚醒消費者線程 pthread_mutex_unlock(&mutex); } pthread_exit(0); } void *consumer(void *ptr) { int i; for (i = 0; i < MAX;i++) { pthread_mutex_lock(&mutex); while( buffer == 0) pthread_cond_wait(&condc, &mutex); buffer = 0; pthread_cond_signal(&condp); pthread_mutex_unlock(&mutex); } pthread_exit(0); }
用消息傳遞解決
下面的例子,用的是信箱法,當線程試圖向一個滿的信箱發信息時,它將會被阻塞,直到有消息被取走
#define N 100 void producer(void) { int item; messgae m; while(1) { item= produce_item(); recieve(comsumer, &m); build_message(&m,item); send(comsumer, &m); } } void comsumer(void) { int item,i; message m; for (i = 0; i < N; i++) send (producer, &m); //發N條空信息給生產者填充,如果不使用緩沖,發一條信息則會被阻塞,如果使用緩沖,則不會被阻塞,而發出去的信號會被緩沖區接受。 while (1) { recieve(producer, &m); item = extract_item(&m); send (producer, &m); comsume_item(item); } }
2. 哲學家就餐問題
哲學家思考問題解決的是互斥訪問有限資源的問題:有N個哲學家坐在一張圓桌子旁邊,只有N個叉子,當哲學家覺得他飢餓了,就會拿起他旁邊的兩個叉子,且不分次序,成功拿起叉子后就會吃飯,吃完后就會放下叉子進行思考。如果旁邊的叉子被拿起,則哲學家就等待叉子被放下后再拿起。

這是個很經典的IPC問題,一個很好的解法如下(而且有很好的並行度)
#define N MAX #define LEFT (i + N - 1)%N #define RIGHT (i + 1)%N #define THINKING 0 #define HUNGRY 1 #define EATING 2 typedef int semaphore int state[N]; //哲學家狀態 semaphore mutex = 1; semaphore s[N]; //哲學家 void take_forks(int i) { down(&mutex); state[i] = HUNGRY; test(i); up(&mutex); down(&s[i]); //哲學家得不到叉子阻塞,如果得到了就不會阻塞(up過了) } void put_forks(int i) { down(&mutex); state[i] = THINKING; test(LEFT); test(RIGHT); up(&mutex); } void test(int i) { if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING) { state[i] = EATING; up(&s[i]); } }
3. 讀寫鎖問題
讀寫鎖的經典例子就是數據庫的訪問,《現代操作系統》里面就有一段這樣的例子代碼,但是這種讀寫鎖的實現方式效率非常低下,而且是讀者優先的解法。
typedef int semaphore semaphore mutex = 1; semaphore db = 1; int rc = 0; //由mutex保護 void writer(void) { while(1) { think_up_data(); down(&db); write_data(); up(&db); } } void reader(void) { while(1) { down(&mutex); rc = rc + 1; if (rc == 1) down(&db); //這里就體現讀者優先了 up(&mutex); read_data(); down(&mutex); rc = rc - 1; if (rc == 0) up(&db); up(&mutex); use_data_read(); } }
《現代操作系統 第三版》 -Andrew S. Tanenbaum