1. 什么是線程
在上一篇進(jìn)程里已經(jīng)講到,可以將一個程序里互不影響又能單獨(dú)拆分出來的任務(wù)放到新進(jìn)程里執(zhí)行,但每個進(jìn)程都有獨(dú)立的地址空間,資源是互相隔離的(如果不用共享內(nèi)存的話),那么如果想用多個進(jìn)程同時處理一個文件怎么辦呢?由此引入了Thread線程的概念。線程就是進(jìn)程中的一段代碼片段,該代碼片段可以和其他代碼片段并發(fā)執(zhí)行,除了一些線程獨(dú)有的執(zhí)行信息,線程能夠共享進(jìn)程的所有資源。
2. 為什么需要線程
- 可以將一個進(jìn)程中IO操作和CPU操作分離開,既能共享地址空間和文件數(shù)據(jù)又能提升效率
比如一個文件編輯的應(yīng)用,用一個線程處理用戶輸入,一個線程定時寫入硬盤實現(xiàn)自動保存,一個線程在后臺做拼寫檢查。如果只有一個線程,保存時就不能處理輸入或檢查,只能順序處理。 - 創(chuàng)建線程的開銷遠(yuǎn)低于進(jìn)程
- 充分利用多核CPU或多CPU計算機(jī)的計算能力
3. 線程模型
經(jīng)典線程模型
除了線程執(zhí)行過程信息是線程獨(dú)有的,其他都是和process共享的。下表第二列是線程獨(dú)有的信息,稱作Thread Control Block (TCB):program counter是線程要執(zhí)行的下一條指令的地址;registers保存了當(dāng)前執(zhí)行指令的數(shù)據(jù)或訪存地址等;方法調(diào)用棧中每一個棧幀就是一個未返回的方法調(diào)用,保存了局部變量和返回地址;線程的狀態(tài)的進(jìn)程一樣也分為running, block, ready和terminated.
| Per-process items | Per-thread items |
|---|---|
| Address space | Program counter |
| Global variables | Registers |
| Open files | Stack |
| Child processes | State |
| Pending Alarms | |
| Signals and signal handlers | |
| Accounting information |
線程的生命周期
- 創(chuàng)建線程:thread_create
- 終止線程:thread_exit
- 等待其他線程終止:thread_join,當(dāng)前線程進(jìn)入block狀態(tài)
- 主動放棄CPU:thread_yield, 當(dāng)前線程進(jìn)入ready狀態(tài)
POSIX 線程
為了能編寫出標(biāo)準(zhǔn)化的可移植的多線程程序,IEEE定義了一個標(biāo)準(zhǔn),Pthreads. 這個標(biāo)準(zhǔn)定義了60多個方法,我們介紹其中幾個主要的:
- Pthread_create
創(chuàng)建一個線程并返回線程標(biāo)識符 - Pthread_exit
終止一個線程并釋放它的stack - Pthread_join
當(dāng)前線程進(jìn)入block狀態(tài),等待其他線程執(zhí)行結(jié)束 - Pthread_yield
主動放棄CPU使用權(quán),進(jìn)入ready狀態(tài) - Pthread_attr_init
創(chuàng)建一個線程相關(guān)的屬性結(jié)構(gòu),并初始化為默認(rèn)值 - Pthread_attr_destroy
刪除線程的屬性結(jié)構(gòu)信息,釋放內(nèi)存資源,但線程本身仍然存在
4. 線程實現(xiàn)
線程可以在用戶空間或內(nèi)核空間實現(xiàn),也可以混合實現(xiàn),接下來我們分別討論不同的實現(xiàn)及其優(yōu)缺點。
在用戶空間實現(xiàn)線程
線程完全在用戶空間實現(xiàn),OS 內(nèi)核對線程的存在一無所知,對OS來說它管理的還是一個個進(jìn)程。進(jìn)程需要自己管理線程,維護(hù)thread table和TCB。
優(yōu)點
- 可以在不支持線程的OS上執(zhí)行
- 線程切換不用切換到內(nèi)核態(tài)執(zhí)行,節(jié)省開銷,速度更快
- process可以自定義線程調(diào)度算法
缺點
- 實現(xiàn)阻塞式系統(tǒng)調(diào)用變得復(fù)雜:比如一個線程等待鍵盤輸入時,不能調(diào)用OS提供的blocking system call,因為這會導(dǎo)致整個process 被block
- page fault會導(dǎo)致process被block: 如果一個線程遇到了缺頁錯誤,OS會將整個process block去做訪存操作,即使其他的線程可以處在ready狀態(tài)。
- 由于時鐘中斷作用不到process內(nèi)部,線程之間切換只能依賴于線程主動放棄CPU使用權(quán)
在內(nèi)核空間實現(xiàn)線程
線程在kernel實現(xiàn),由kernel管理thread table和TCB。線程的創(chuàng)建和終止都通過system call實現(xiàn)。
優(yōu)點
- 可能導(dǎo)致線程block的調(diào)用都由system call實現(xiàn)
- 一個thread block之后,調(diào)度器可以選擇相同process中的其他線程繼續(xù)執(zhí)行,也可以選擇不同process的線程執(zhí)行
缺點
- 每次system call切換到kernel開銷巨大:針對這個缺點的解決方案是使用線程池,一個線程執(zhí)行結(jié)束之后不直接銷毀而是進(jìn)入idle狀態(tài)等待新的任務(wù)。
混合實現(xiàn)
為了平衡用戶空間和內(nèi)核空間實現(xiàn)線程的優(yōu)缺點,可以采用在用戶空間和內(nèi)核空間混合實現(xiàn)的方式,開發(fā)人員決定使用多少kernel thread 和 user-level thread。
編寫多線程程序要考慮的問題
- 如何處理屬于某個線程的全局變量,而讓其他線程不可見
比如errno. thread1 要打開一個文件前去檢查對文件的permission,OS返回結(jié)果到errno, 此時CPU使用權(quán)轉(zhuǎn)到thread2, thread2執(zhí)行操作也向全局變量errno寫入覆蓋了thread1的結(jié)果,thread1恢復(fù)執(zhí)行以后去errno取到錯誤的結(jié)果導(dǎo)致執(zhí)行失敗。
解決辦法:每個thread維護(hù)自己的私有全局變量 - 如何避免多個線程同時進(jìn)入一個不可重入的方法,比如malloc
- signal問題:比如一個鍵盤中斷發(fā)出signal又沒有指定線程時,應(yīng)該由哪個線程來捕獲這個信號
- 棧管理問題:一個進(jìn)程棧溢出時,OS會自動分配stack空間,但如果一個進(jìn)程擁有多個線程,每個線程都有自己的??臻g,如果kernel不知道這些stack的存在就無法正確地自動分配更多空間。