操作系統與網絡 (9. Linux多線程Thread)

時間是一個偉大的作者,它會給每個人寫出完美的結局來!

9. Linux多線程Thread

9.1 線程初始

9.1.1 線程的基本概念

(1)線程是一個執行流,用於運行代碼以及處理數據;
(2)Linux下的線程:用戶態線程+輕量級進程;
用戶態線程:使用庫函數實現創建的線程,這個用戶態線程在內核中使用一個輕量級進程實現調度;

9.1.2 線程與進程
  • 線程
    (1)Linux下線程是CPU調度的基本單位——因爲Linux是線程;
  • 進程
    (1)Linux下進程是資源分配的基本單位——因爲程序運行時資源勢分配給整個線程組(進程)的;
    (2)傳統操作系統中使用pcb來描述一個程序的運行——pcb就是進程,LInux下使用pcb來模擬實現線程,因此Linux下的pcb實際上是一個輕量級進程LWP(輕量級進程是因爲公用大部分進程資源,相較於傳統進程更加輕量化)。
9.1.3 線程的優缺點
  • 線程的優點
    (1)創建一個新線程的代價要比創建一個新進程小得多;
    (2)與進程之間的切換相比,線程之間的切換需要操作系統做的工作要少很多;
    (3)線程佔用的資源要比進程少很多;
    (4)能充分利用多處理器的可並行數量;
    (5)在等待慢速I/O操作結束的同時,程序可執行其他的計算任務;
    (6)計算密集型應用,爲了能在多處理器系統上運行,將計算分解到多個線程中實現;
    (7)I/O密集型應用,爲了提高性能,將I/O操作重疊。線程可以同時等待不同的I/O操作。
  • 線程的缺點
    (1)性能損失(增加了額外的同步和調度開銷,而可用的資源不變);
    (2)健壯性降低(線程之間是缺乏保護的);
    (3)缺乏訪問控制(進程是訪問控制的基本粒度,在一個線程中調用某些OS函數會對整個
    進程造成影響);
    (4)編程難度提高;
    (5)編寫與調試一個多線程程序比單線程程序困難得多。
9.1.4 線程之間資源的獨有與共享
  • 獨有
    棧、寄存器、、信號屏蔽字、errno、進程ID、調度優先級;
  • 共享
    共享虛擬地址空間、文件描述符表、信號處理方式、當前的工作路徑、用戶ID、組ID。

9.2 線程控制

9.2.1 線程創建

(1)功能
創建一個新的線程
(2)原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(start_routine)(void), void *arg);
(3)參數
thread: 返回線程ID
attr: 設置線程的屬性,attr爲NULL表示使用默認屬性
start_routine: 是個函數地址,線程啓動後要執行的函數
arg: 傳給線程啓動函數的參數
(4)返回值
成功返回0;失敗返回錯誤碼。

9.2.2 線程終止

1. pthread_exit
(1)功能
線程終止;
(2)原型
void pthread_exit(void *value_ptr);
(3)參數
value_ptr:value_ptr不要指向一個局部變量;
(4)返回值
無返回值,跟進程一樣,線程結束的時候無法返回到它的調用者(自身)。
2. pthread_cancel
(1)功能
取消一個執行中的線程;
(2)原型
int pthread_cancel(pthread_t thread);
(3)參數
thread:線程ID
(4)返回值
返回值:成功返回0;失敗返回錯誤碼。
3. 線程終止的三種方法

  1. 從線程函數return;這種方法對主線程不適用,從main函數return相當於調用exit;
  2. 線程可以調用pthread_exit終止自己;
  3. 一個線程可以調用pthread_cancel終止同一進程中的另一個線程。
  4. 線程退出之後,默認不會自動釋放資源(保存自己的退出結果在線程的地址空間中),會造成資源泄露;
  5. 主線程退出之後,其他線程依然可以正常運行。
9.2.3 線程等待

(1)功能
阻塞等待指定線程退出,通過retval獲取返回值;
(2)原型
int pthread_join(pthread_t thread, void **value_ptr);
(3)參數
thread:線程ID;
value_ptr:它指向一個指針,後者指向線程的返回值;
(4)返回值
成功返回0;失敗返回錯誤碼。
(5)pthread線程一不同的方法終止,通過pthread_join得到終止狀態不同即爲:

  • 如果thread線程通過return返回,value_ ptr所指向的單元裏存放的是thread線程函數的返回值;
  • 如果thread線程被別的線程調用pthread_ cancel異常終掉,value_ ptr所指向的單元裏存放的是常數PTHREAD_CANCELED。
  • 如果thread線程是自己調用pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的參數;
  • 如果對thread線程的終止狀態不感興趣,可以傳NULL給value_ ptr參數。

(6)說明
一個線程創建出來之後,默認有一個joinable屬性,處於joinable屬性的線程退出之後,不會自動釋放資源,需要被其他線程等待,才能釋放資源;處於joinable屬性的線程必須等待,否則造成資源泄露。

9.2.4 線程分離

(1)功能
將線程joinable屬性修改爲detach屬性
(2)原型
int pthread_detach(pthread_t thread);
(3)說明

  1. 線程若屬於detach屬性,則線程將退出後將自動回收資源;
  2. 這個線程將不再需要等待(因爲線程退出,返回值佔用的空間已經被回收了);

(4)應用場景
可以在任意線程中實現(對線程的返回值並不關心)。

9.2.5 說明

(1)Linux下操作系統並沒有提供線程的控制系統調用接口;
(2)因此大佬們封裝了一個線程控制接口庫;
(3)Linux的線程:用戶態線程+輕量級進程。
(4)要使用這些函數庫,要通過引入頭文<pthread.h>;
(5)鏈接這些線程函數庫時要使用編譯器命令的“-lpthread”選項。

9.2.6 線程中ID的討論

(1)tid——線程地址空間的首地址;
(2)pcb——pid(輕量級進程Id,LWP);
(3)pcb——tgid(線程、進程組id,默認等於首線程id)。

9.3 線程安全

9.3.1 概念

線程安全 : 多個線程同時對臨界資源進行訪問而不會造成數據二義性.
重入 : 同一個函數被不同的執行流調用,當前一個流程還沒有執行完,就有其他的執行流再次進入,我們稱之爲重入.
可重入函數 : 一個函數在重入的情況下,運行結果不會出現任何不同或者任何問題;

9.3.2 線程安全的實現

(1) 同步

  • 概念
    保證對臨界資源訪問的時序合理性
  • 實現 (等待+喚醒)
    條件變量
    線程在對臨界資源進行訪問之前, 首先判斷是否能夠操作,若可以操作則線程直接操作; 若不可以操作, 則條件變量提供等待功能, 讓pcb等待在等待隊列上, 其他線程促使操作條件滿足, 然後喚醒條件變量等待隊列上的線程.
    條件變量 = 等待 + 喚醒 +等待隊列
    信號量
    等待與喚醒的功能,不具備操作條件的時候則等待,其它線程促使條件滿足後,喚醒等待的線程。

(2) 互斥

  • 概念
    保證對臨界資源同一時間訪問的安全性(唯一性).

  • 實現 (互斥鎖)
    互斥鎖

  • 原理
    通過對一個只有0/1的計數器的原子判斷
    死鎖

  • 概念
    指在一組進程中, 多個線程對多個鎖資源進行爭搶操作,但是因爲推進
    順序不當,造成環路等待導致程序流程無法推進.

  • 產生的必要條件

    • 互斥條件
      一個資源每次只能被一個執行流使用
    • 不可剝奪條件
      一個執行流已獲得的資源,在末使用完之前,不能強行剝奪
    • 請求與保持條件
      一個執行流因請求資源而阻塞時,對已獲得的資源保持不放
    • 環路等待條件
      若干執行流之間形成一種頭尾相接的循環等待資源的關係

    預防死鎖

    1. 破壞死鎖的四個必要條件
    2. 加鎖順序一致
    3. 避免鎖未釋放的場景
    4. 資源一次性分配

    避免死鎖算法
    銀行家算法
    死鎖檢測算法

信號量

  • 具體實現
    1. 定義互斥鎖變量
    pthread_mutex_t *mutex;
    2. 對互斥鎖變量進行初始化
    靜態分配
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
    動態分配
    int pthread_mutex_init(pthread_mutex_t restrict mutex,
    const pthread_mutexattr_t
    restrict attr);
    參數:mutex:要初始化的互斥量
    attr:NULL
    3. 對臨界資源操作之前先加鎖
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    返回值:成功返回0,失敗返回錯誤號
    4. 對臨界資源操作完畢後進行解鎖
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    返回值:成功返回0,失敗返回錯誤號
    5. 銷燬互斥鎖
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    使用PTHREAD
    MUTEX_ INITIALIZER初始化的互斥量不需要銷燬;
    不要銷燬一個已經加鎖的互斥量;
    已經銷燬的互斥量,要確保後面不會有線程再嘗試加鎖;
9.3.3 生產者與消費者模型(321)
  • 生產者消費模型優點
  1. 解耦和
    緩衝區作爲中間過渡作用
  2. 支持忙閒不均
    緩存數據, 緩慢處理
  3. 支持併發
    多個生產者可同時向裏面放置數據
  • 實現
  1. 一個場所
    緩衝區 (需要保證安全)
  2. 兩類角色
    生產者, 消費者
  3. 三種關係
    生產則與消費者之間具有同步與互斥關係;
    生產者與生產者之間具有互斥關係;
    消費者與消費者之間具有互斥關係;

9.4 線程池

9.4.1 概念

(1) 一堆線程+任務隊列 (緩衝任務)
(2) 在程序初始化時, 創建固定數量的線程 (最大數量限制),從任務隊列中獲取任務, 進行處理.
(3) 一種線程使用模式。線程過多會帶來調度開銷,進而影響緩存局部性和整體性能。而線程池
維護着多個線程,等待着監督管理者分配可併發執行的任務。這避免了在處理短時間任務時創建與銷燬線程的代價。線程池不僅能夠保證內核的充分利用,還能防止過分調度。可用線程數量應該取決於可用的併發處理器、處理器內核、內存、網絡sockets等的數量。

9.4.2 作用

(1) 避免爲大量請求創建線程, 導致資源耗盡程序奔潰的問題;
(2) 避免大量線程頻繁創建以及銷燬所帶來的時間成本;

9.4.3 實現

線程創建 + 線程安全的任務隊列實現

9.4.4 線程池的銷燬條件

所有線程退出之後

9.4.4 線程池的應用場景

(1) 需要大量的線程來完成任務,且完成任務的時間比較短。WEB服務器完成網頁請求這樣的任務,使用線程池技術是非常合適的。因爲單個任務小,而任務數量巨大,你可以想象一個熱門網站的點擊次數。 但對於長時間的任務,比如一個Telnet連接請求,線程池的優點就不明顯了,因爲Telnet會話時間比線程的創建時間大多了。
(2) 對性能要求苛刻的應用,比如要求服務器迅速響應客戶請求。
(3) 接受突發性的大量請求,但不至於使服務器因此產生大量線程的應用。突發性大量客戶請
求,在沒有線程池情況下,將產生大量線程,雖然理論上大部分操作系統線程數目最大值不是問題,短時間內產生大量線程可能使內存到達極限,出現錯誤.

9.5 線程安全的單例模式

9.5.1 設計模式

(1) 定義
大佬們對典型的應用場景設計出的解決方案
(2) 分類
(3) 使用場景

9.5.2 單例模式

是設計模式中的一種常見模式, 一個對象只能被實例化一次 (一個資源只能被加載一次).

9.5.3 實現

餓漢模式
懶漢模式
資源在使用的時候才加載, 在多個執行流中就可能被加載多次, 因此資源的加載/對象的實例化過程需要保護,線程安全的單例模式主要針對的是懶漢方式的實現.
非線程安全+volatile

  • 實現線程安全
class info{
static T* _data;
T* get_instance(){
	lock();
	if(NULL == _data){
		_data = new T();
	}
	unlock();
	return _data;
	}
}

避免鎖衝突進行二次判斷

9.6 常見的鎖

9.6.1 常見的鎖的種類

(1) 悲觀鎖
在每次取數據時,總是擔心數據會被其他線程修改,所以會在取數據前先加鎖(讀鎖,寫鎖,行鎖等),當其他線程想要訪問數據時,被阻塞掛起。
(2) 樂觀鎖
每次取數據時候,總是樂觀的認爲數據不會被其他線程修改,因此不上鎖。但是在更新數據
前,會判斷其他數據在更新前有沒有對數據進行修改。
(3) 互斥鎖
(4) 自旋鎖
(5) 讀寫鎖

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