線程是最小執行單位, 進程是最小分配資源單位. 線程由進程退化而來(進程調用pthread_create), 多個線程同享同一份虛擬地址空間, 每一個線程有一個獨立的PCB. 對CPU而言, 進程和線程的地位是等同的(都有PCB), 所以一個進程退化成多個線程后能夠分到更多的時間片, 從而提高效率. 各線程共享文件描述符表和除棧以外的用戶地址空間; 不共享內核棧/用戶棧,信號屏蔽字,errno變量.
使用多線程模型時應避免使用fork()和信號, 否則容易出錯.
三級頁表
PCB內頁目錄的指針指向中間頁目錄(4k), 中間頁目錄的每一個指針指向一個頁表, 頁表的每一個指針又指向一個物理頁面, 物理頁面內部就是存儲數據的內存單元. 頁目錄 -> 中間頁目錄 -> 頁表 -> 物理頁面, 該結構稱為三級頁表. 物理頁面和虛擬地址空間通過MMU映射起來, MMU記錄頁碼和頁框碼以及頁內偏移量, 經常訪問的頁面會存放到快表寄存器中.
棧幀和寄存器
線程雖然獨享PCB, 但是它的頁目錄頁表還是跟原進程相同的, 所以多個線程共享虛擬地址空間, 指向相同的內存. 線程負責執行函數, 所以可以看做寄存器和棧的集合, 每一個函數在棧空間內都有一個棧幀(通過寄存器指針esp - ebp得到), 由于對應的寄存器只有一個, 所以棧幀內會存儲之前ebp/esp的值. 線程也要在內核棧內保存寄存器的值, 用來切換線程.
線程控制原語
pthread庫是第三方庫, 編譯鏈接時要加上-lpthread參數.
pthread_self()
查看進程ID, 相當于getpid(). 注意線程號LWP不是線程ID, 線程號是CPU分配時間片的依據, 線程ID用來在進程中區分線程. 使用ps -Lf pid
查看線程號, 其中pid是進程號, 可以看到該進程下各線程的LWP.
pthread_create(&tid, NULL, 線程主函數, 線程主函數參數)
創建線程, 相當于fork(). 成功返回0, 失敗返回錯誤號, 不是-1, 可以使用strerror(errno)
打印錯誤信息. 線程主函數為void* (*func)(void*)
類型的函數指針. 參數必須傳值進去如(void*)i, 傳地址可能是在爭搶中其他線程的參數.
調用pthread_create()的進程退化為主控線程, 與其他線程的pid相同, 線程ID各不同.
pthread_exit((void*)retval)
單個線程自己退出, 等價于exit(), 參數retval為線程退出時傳出的值. 線程中最好不要使用return, 絕不能使用exit(), exit()會將整個進程退出.
pthread_join(tid, (void**)&retval)
等價于waitpid(), 阻塞等待線程關閉, 回收線程, 可以是兄弟線程調用. 傳出參數&retval同上, 類似于&status, 也是線程退出時傳出的值. 根據如何退出的retval會存放不同的值, 如return存放返回值, pthread
_cancel()存放PTHREAD_CANCELED, pthread_exit()存放exit傳出的retval.
pthread_detach(tid)
使線程與主控線程斷開關系, 從而不需要(也不能被)其他進程pthread_join()就可以自己結束并回收所有資源(清理PCB), 不會導致僵尸進程.
pthread_cancel(tid)
等價于kill(), 殺死線程. 線程不會立刻被殺死, 而是在系統調用處如open()/read()后才被殺死. 可以自己添加取消點pthread_testcancel()
.
修改線程屬性
- 修改線程為分離態: 除了detach函數也可以在create的時候就指定為分離態
pthread_attr_t是一個結構體, 需要有初始化init和對應的destroy函數, 在setdetachstate后傳入到create中.
- 修改線程棧的大小
線程同步
互斥量
pthread_mutex_t mutex
定義鎖, 是結構體類型, 可看作只有0/1的整數. 初始化成功為1. 應定義成全局變量.
pthread_mutex_init(&mutex, NULL)
對互斥鎖進行初始化為1, 要在create線程之前, 聲明和初始化如果定義成全局/靜態變量可以用靜態初始化替代pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
, 局部變量必須用動態初始化.
pthread_mutex_destroy(&mutex)
. 銷毀互斥鎖
pthread_mutex_lock(&mutex)
加鎖, 將mutex-1變為0. 加鎖失敗會阻塞.
pthread_mutex_unlock(&mutex)
解鎖, 將mutex+1變為1. 同時喚醒所有阻塞在該鎖上的線程.
pthread_mutex_trylock(&mutex)
嘗試非阻塞加鎖, 如果鎖已被占用則直接返回, 不會像lock一樣阻塞.
在訪問共享資源前加鎖, 訪問結束后立即解鎖, 鎖的粒度應越小越好.
死鎖
-
連續對同一個互斥量加鎖兩次. 第二次請求加鎖時會造成線程阻塞等待, 導致第一次加的鎖無法釋放.
解決方法: 加鎖完立刻解鎖, 第二次加鎖前先檢查是否已經有鎖.
-
線程1擁有著A鎖, 請求獲得B鎖; 線程2擁有著B鎖, 請求獲得A鎖. 對于已經擁有互斥鎖的線程, 再去請求別的鎖, 會導致線程阻塞等待. 雙方都不釋放就會導致死鎖.
預防方法:
- 一次申請兩把鎖, 之后一起釋放.
- 當不能拿到所有的鎖時, 主動釋放已有的鎖, 等別的線程把兩個鎖都釋放后就可以繼續加鎖了.
- 對線程編號.
注意, trylock()不能解決問題, 因為即使不阻塞等待, 但兩個鎖仍未被兩個線程釋放, 所以仍然無法得到對方的鎖.
死鎖解決方法:
除了預防, 還可以1.搶占資源分配給死鎖線程. 2.殺死線程, 打破循環.
哲學家吃飯問題:
策略1: 規定奇數號的哲學家先拿起他左邊的筷子,然后再去拿他右邊的筷子;而偶數號的哲學家則先拿起他右邊的筷子,然后再去拿他左邊的筷子。按此規定,將是0、1號哲學家競爭1號筷子,2、3號哲學家競爭3號筷子。即五個哲學家都競爭奇數號筷子,獲得后,再去競爭偶數號筷子,最后總會有一個哲學家能獲得兩支筷子而進餐。
策略2: 至多只允許四個哲學家同時進餐,以保證至少有一個哲學家能夠進餐,最終總會釋放出他所使用過的兩支筷子,從而可使更多的哲學家進餐。定義信號量count,只允許4個哲學家同時進餐,這樣就能保證至少有一個哲學家可以就餐。一個主動放棄自己(可能)擁有的鎖, 使別的線程獲得. 使用信號量實現.
讀寫鎖
讀寫鎖同時具備兩種狀態: 讀鎖和寫鎖. 其特性為: 寫獨占, 讀共享; 寫鎖優先級高.
舉例: 當已經有線程對共享資源加了讀鎖時, 若其他線程也都是讀者, 以讀模式對其加鎖的線程可以共享該線程的讀鎖, 一起讀; 若其他線程都是寫者, 則寫者都阻塞; 若其他線程有讀鎖有寫鎖, 則寫鎖優先.
若共享資源已被加了寫鎖, 在寫的時候別的線程都無法訪問共享資源, 等到寫鎖釋放后, 其他線程再爭奪鎖, 這時仍是寫鎖優先.
一個讀寫鎖同時只能有一個寫者或多個讀者(與CPU數相關), 當有多個寫者競爭時, 只有一個能獲得寫鎖, 其他的寫者下一次獲得鎖.
pthread_rwlock_init()
初始化
pthread_rwlock_destroy()
銷毀, 主控線程內最后執行
pthread_rwlock_rdlock()
讀鎖
pthread_rwlock_tryrdlock()
嘗試讀
pthread_rwlock_wrlock()
寫鎖
pthread_rwlock_trywrlock()
嘗試寫
pthread_rwlock_unlock()
解鎖, 不區分讀寫
rdlock若申請不到鎖,則自旋(隔一會再看看),tryrdlock若申請不到鎖,則返回(不會再嘗試),由用戶自旋.
條件變量
條件變量不是鎖, 但會造成線程阻塞. 通常與互斥鎖配合使用, 實現生產者消費者模型. 條件變量的優點是減少生產者和消費者,消費者和消費者之間對互斥量無意義的競爭, 使線程掛起釋放CPU, 提高效率.
pthread_cond_t cond
條件變量, 結構體類型
pthread_cond_init(cond, NULL)
初始化, 聲明和初始化如果定義成全局/靜態變量也可以用靜態初始化替代pthread_cond_t cond = PTHREAD_COND_INITIALIZER
pthread_cond_destroy(cond)
摧毀
pthread_cond_wait(cond, mutex)
阻塞等待條件變量并釋放鎖, 當被喚醒時才重新加鎖. 在調該函數之前需要線程已加互斥鎖, 再判斷是否符合條件變量(如共享資源為空)來阻塞并釋放鎖.
pthread_cond_timedwait(cond, mutex, abstime)
阻塞一定時間就不等了, abstime是一個timespec結構體, 有tv_sec(秒)和tv_nsec(納秒)兩個成員. 絕對時間是相對于1970年1月1日00:00:00來說, 需要先使用time_t cur = time(NULL)
獲取當前時間, 再把cur+n的偏移量賦值給結構體成員tv_sec.
pthread_cond_signal(cond)
喚醒至少一個阻塞在條件變量的線程.
pthread_cond_broadcast(cond)
喚醒所有阻塞在條件變量的線程.
生產者生產后條件變量就滿足了, 之后使用signal()/broadcast()喚醒阻塞在條件變量的消費者線程, 不過消費者線程在訪問共享數據時仍要先獲得鎖.
信號量
int sem_init(sem_t *sem, int pshared, unsigned int value)
sem_t雖然是結構體, 但可以近似看作是整數. 由于進程和線程都可以使用信號量, pshared用于指定信號量是否可以在進程間共享(一般傳0). value是信號量的初值, 當取1時和互斥量相同. 不同于線程系列函數, 失敗返回-1并設置errno.
sem_destroy(sem)
銷毀信號量.
在用信號量實現生產者消費者模型時, 生產者先sem_wait(&sem_blank)
使共享資源的空格信號量減少, 然后生產, 之后再sem_post(&sem_product)
使產品信號量增加. 對應消費者那邊先sem_wait(&product)
使產品信號量減少, 然后消費走, 再sem_post(&sem_blank)
使空格信號量增加.
共享資源的環形隊列可以用數組實現, i = (i+1)%NUM, 通過取余實現邏輯上的環.