本篇介紹一些Linux線程相關內容.
1. 多線程管理
進程是具有獨立功能的程序在某個數據集合上的一次運行活動, 進程是系統進行資源分配和調用的獨立單位
線程是進程的一個實體, 它是競爭分配 CPU 的基本單位
進程和線程的區別
(1) 線程是進程的一部分, 也被稱作輕量級的進程
(2) 進程資源分配的基本單位, 線程是 CPU 調度的基本單位
(3) 線程的創建比進程的創建系統開銷小
網絡服務器程序通常需要處理多個客戶端的請求, 使用的併發的技術無非就是多進程和多線程
多進程技術存在很多的侷限性, 比如: 要分配大量的資源, 進程 PID, 代碼段,數據段,堆區, 棧區..., 其系統消耗是非常大的, 會嚴重影響服務器的處理速度, 因此在網絡編程過程中越來越多的使用線程技術, 實現併發處理
線程是一種輕量級的進程, 對系統資源的需求非常少, 線程隸屬於某個進程, 進程內部可以由很多線程, 線程共享進程的資源, 比如: 代碼段 , 數據段, 堆區, 環境變量表, 文件描述符...
每個線程只需要建立一個獨立的棧區就可以了(除棧區外的其他內容共享)
每個進程內部都至少有一個線程, 叫做主線程( main() ), 主線程一旦結束, 進程隨之結束, 進程結束該進程所擁有的所有線程
每個線程的內部到碼都是順序執行的, 多線程之間的代碼亂序執行
線程並行的原理: 宏觀上並行, 微觀上串行
***** 進程是資源分配的單位, 如代碼區 數據段等
***** 線程是競爭分配 CPU 的基本單位
2. linux/unix 關於多線程的編程
POSIX (可移植操作系統接口) 標準 : 是IEEE為要在各種UNIX操作系統上運行軟件,而定義API的一系列互相關聯的標準的總稱
unix/linux 系統中提供的系統調用遵循了 POSIX 標準形式,因此係統中給應用工程師提供的系統函數的API ( 應用程序編程接口), 函數名, 參數是統一的, 只要是在 unix/linux 系統, 都能通用
2.1 多線程的創建
編碼時使用頭文件<pthread.h>, 編譯鏈接時使用共享庫 pthread/<pthread.h>
int pthread_create ( pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
thread : 指向線程標識符的指針, 返回創建的新線程的 ID
ID 是線程的唯一標識, 本質就是一個正整數, 是一個傳出參數
attr : 創建新線程時可以指定線程的某些屬性
一般給 0( NULL) 即可, 代表使用默認屬性
start_routine : 函數指針, 指向了線程執行的主體, 線程運行函數的地址
arg : 運行函數的參數, 傳給 start_routine 的參數
返回值 : 成功返回0, 失敗返回錯誤編碼
注意: 在多線程中儘量不要使用 errno, 因為獲取的 errno 有可能不是錯誤的原因
ps -Lf + PID : 查看對應的進程中有幾個線程
2.2 多線程的運行
2.2.1 獲取線程的 ID
pthread_t pthread_self ( void );
pthread_self() : 獲取線程的 ID, 返回值就是線程的 ID
成功返回調用線程的 ID, 這個函數不會失敗
主線程退出, 會導致進程退出, 致使子線程得不到執行, 這種情況實際編程過程中要避免出現, 如果出現該情況, 它屬於編程邏輯問題, 編譯器檢出不出來
2.2.2 線程間的線程傳遞
(1) 全局變量
(2) ptread_create 函數的第四個參數
2.3 多線程的結束
正確的應該先是子線程結束, 然後後結束主線程
2.3.1 主線程如何監控到子線程的結束
pthread_join 函數
int pthread_join ( pthread_t thread, void **retval );
作用 : 阻塞的方式等待thread指定的線程結束,當函數返回時,被等待線程的資源被收回,如果線程已經結束,那麼該函數會立即返回
thread : 參數為被等待的線程標識符
retval : 用戶定義的指針,它可以用來存儲被等待線程的返回值。
返回值 : 成功返回0, 失敗返回錯誤編碼
2.3.2 子線程結束時的返回值問題
如何得到子線程的返回值
(1) 全局變量, 把子線程的返回值放入某一個全局變量, 主線程再去查看
(2) 通過線程函數的返回值
線程函數通過強轉得到的( void*)的返回值會傳給 pthread_join 的第二個參數,當 pthread_join()函數第二個參數不為 0 的情況下
(3) 通過 pthread_create 函數的第四個參數
2.3.3 子線程是如何結束的
(1) 線程的主體函數中執行 return
(2) pthread_exit 函數
void pthread_exit (void*);
如果在線程主體函數中執行return (void *)-1 等價於 pthread_exit ((void*)-1)
(3) 在進程的任何位置執行 exit 都會導致整個進程的結束
2.4 線程資源的回收
線程的資源 : 棧空間 線程的返回值
如何處理線程資源
(1) 置之不理
等到進程結束後自動回收, 該線程資源什麼時候被回收是不確定的
(2) pthread_join 函數
pthread_join 函數等待特定線程的結束( 不管 pthread_join 的第二個參數是否為 0),只要該函數返回, 結束線程的資源就會被立刻回收
(3) pthread_detach 函數
將某個線程設置為分離狀態, 設置為分離狀態的線程一旦結束, 佔據的資源會被立刻回收
因為 pthread_join 函數要回收子線程資源的話, 就必須要等待子線程的結束, 那麼主線程就得等待, 如果此時主線程還有其他工作要完成, 這樣就會浪費資源
int pthread_detach ( pthread_t thread );
thread : 要把哪個線程設置為分離狀態
返回值 : 成功返回0, 失敗返回錯誤編碼
(4) pthread_join / pthread_detach 使用
a) 一個新創建的線程最好要麼 pthread_join 要麼 pthread_detach
b) 一個設置為分離狀態的線程, 再去對它調用 join 就沒有效果了
c) 需要等待的線程使用 pthread_join, 不需要等帶的線程使用 pthread_detach
3. 線程的同步
3.1 線程同步的原因
多個線程共享數據段\BSS 段\堆區, 當定義一個全局變量 cnt, 該變量對於進程中所有線程都可見, 都可以修改 cnt, 子線程同時操作時會產生衝突, 導致 cnt 的結果不確定
3.2. 互斥量
本質就是一把鎖( 可以參考文件鎖)
編程步驟:
(1) 定義一個互斥量
pthread_mutex_t lock;
(2) 初始化互斥量
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t * attr);
mutex : 指向要初始化的互斥量
attr : 互斥鎖屬性,NULL表示缺省屬性
返回值 : 成功時返回0,失敗時返回錯誤碼
(3) 加鎖臨界區, 保證只有一個線程可以使用共享資源
int pthread_mutex_lock(pthread_mutex_t *mutex);
mutex : 指向要加鎖的互斥鎖對象
如果加鎖成功, 立即返回
如果 lock 已經處於所得狀態, 該函數阻塞等待, 直到 lock 處於unlock 狀態, 加鎖成功並返回
返回值 : 成功時返回0,失敗時返回錯誤碼
pthread_mutex_trylock()函數加鎖不成功立刻返回
(4) 操作共享資源
對共享資源進行操作,此時只有當前線程在操作,因此不會發生衝突
(5) 解鎖/釋放鎖
int pthread_mutex_unlock(pthread_mutex_t *mutex);
mutex : 指向要解鎖的互斥鎖對象
返回值 : 成功時返回0,失敗時返回錯誤碼
(6) 回收互斥量/互斥鎖
int pthread_mutex_destroy(pthread_mutex_t *mutex);
mutex : 指向要銷燬的互斥鎖對象
返回值 : 成功時返回0,失敗時返回錯誤碼
死鎖產生
1) 一個線程試圖對一個互斥量加兩次鎖
2) 線程一獲取了 A 鎖 再試圖獲取 B 鎖
3) 線程二獲取了 B 鎖 再試圖獲取 A 鎖, 相互之間有衝突
避免死鎖
1) 加鎖時按順序加鎖
2) 使用pthread_mutex_trylock()函數
線程一 trylock 獲取 A 鎖, 再試圖 trylock B 鎖, 如果不成功釋放 A 鎖
3.3 信號量
信號量集用於進程之間的同步操作
信號量是用於線程之間的同步操作
信號量本質上就是一個計數器(int), 可以用於控制訪問共享資源的最大線程數,當信號量的值為 1 時, 等同於互斥量
信號量的編程步驟:
(1) 定義一個信號量
sem_t sem;
(2) 初始化信號量
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem : 需要初始化的信號量 sem
pshared : 控制信號量的類型,
=0, 代表該信號量用於多線程間的同步
>0, 表示可以共享,用於多個相關進程間的同步
value : 要初始化的目標值, 也就是要把信號量 sem 初始化為多少
返回值 : 成功返回0, 失敗返回-1, 錯誤信息存於errno
(3) 獲取信號量
拿到信號量就可以操作共享資源, 拿不到信號量就不能操作共享資源
本質上就是給 計數-1 信號量-1
int sem_wait(sem_t *sem); /*信號量-1, 不夠減的話則阻塞等待*/
int sem_trywait(sem_t *sem); /*信號量-1, 不夠減返回出錯信息*/
int sem_timedwait(sem_t *sem, timeout); /*信號量-1, 不夠減阻塞等待, 直到夠減或者等待時間超時*/
(4) 操作共享資源
(5) 釋放信號量
本質上就是給 計數+1 信號量+1
int sem_post(sem_t *sem); /*指定的信號量 sem 的值加 1*/
(6) 回收信號量
int sem_destroy(sem_t *sem); /*對用完的信號量的清理*/
3.4 條件變量
生產者消費者模型
生產者向循環緩衝區放入產品,如果緩衝區滿阻塞,當緩衝區不滿的時繼續向其中放入產品
消費者從循環緩衝區取出產品,如果緩衝區空阻塞,當緩衝區不空的時繼續從其中取出產品
循環緩衝區是共享資源, 需要使用互斥量加鎖訪問
條件變量是用來等待線程而不是上鎖的,條件變量通常和互斥鎖一起使用
條件變量之所以要和互斥鎖一起使用,主要是因為互斥鎖的一個明顯的特點就是它只有兩種狀態:鎖定和非鎖定,而條件變量可以通過允許線程阻塞和等待另一個線程發送信號來彌補互斥鎖的不足,所以互斥鎖和條件變量通常一起使用
編程步驟
(1) 定義條件變量
pthread_cond_t cond;
(2) 初始化條件變量
int pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *cattr);
cv : 要初始化的條件變量
cattr : 條件變量的屬性, 通常給 0 就可以了
(3) 暫停線程, 使調用線程睡入條件變量 cond 指向的隊列, 同時釋放互斥鎖
int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *mutex);
cv : 把線程放入條件變量 對應的隊列
mutex : 要釋放那個的互斥鎖
(4) 喚醒線程/解除暫停, 把線程從條件變量指定的隊列中取出
int pthread_cond_signal(pthread_cond_t *cv);
cv : 喚醒響應的線程
(5) 銷燬條件變量
int pthread_cond_destroy(pthread_cond_t *cv);
cv : 要銷燬的條件變量
閱讀更多 刷新isc 的文章