線程控制原語/線程同步

線程是最小執行單位, 進程是最小分配資源單位. 線程由進程退化而來(進程調用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().

修改線程屬性
  1. 修改線程為分離態: 除了detach函數也可以在create的時候就指定為分離態

pthread_attr_t是一個結構體, 需要有初始化init和對應的destroy函數, 在setdetachstate后傳入到create中.

  1. 修改線程棧的大小

線程同步

互斥量

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. 連續對同一個互斥量加鎖兩次. 第二次請求加鎖時會造成線程阻塞等待, 導致第一次加的鎖無法釋放.

    解決方法: 加鎖完立刻解鎖, 第二次加鎖前先檢查是否已經有鎖.

  2. 線程1擁有著A鎖, 請求獲得B鎖; 線程2擁有著B鎖, 請求獲得A鎖. 對于已經擁有互斥鎖的線程, 再去請求別的鎖, 會導致線程阻塞等待. 雙方都不釋放就會導致死鎖.

    預防方法:

    1. 一次申請兩把鎖, 之后一起釋放.
    2. 當不能拿到所有的鎖時, 主動釋放已有的鎖, 等別的線程把兩個鎖都釋放后就可以繼續加鎖了.
    3. 對線程編號.
      注意, 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, 通過取余實現邏輯上的環.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1.什么是進程 進程是指系統正在運行的一個應用程序。每個進程之間是獨立的,每個進程均運行在其專用且受保護的內存空間...
    tiGress閱讀 361評論 2 1
  • 1.內存的頁面置換算法 (1)最佳置換算法(OPT)(理想置換算法):從主存中移出永遠不再需要的頁面;如無這樣的...
    杰倫哎呦哎呦閱讀 3,327評論 1 9
  • 下面是Java線程相關的熱門面試題,你可以用它來好好準備面試。 1) 什么是線程? 線程是操作系統能夠進行運算調度...
    冰箱哥哥閱讀 527評論 0 2
  • 以下內容整理自互聯網,僅用于個人學習 1. 進程 1.1 進程的定義 進程是程序運行的一次執行過程。 進程是一個程...
    學不好語文的LJ碼農閱讀 255評論 0 1
  • 1.1 并發與并行 并行:指兩個或多個時間在同一時刻發生(同時發生)并發:值兩個或多個時間在同一時間段內發生在操作...
    阿麼閱讀 525評論 0 0