1. 程式人生 > >Linux作業系統——執行緒

Linux作業系統——執行緒

執行緒:在一個程式裡的一個執行路線就叫做執行緒。更準確的定義是:執行緒是一個程序內部的控制序列。
一切程序至少都有一個執行執行緒。
程序和執行緒:
程序是資源競爭的基本單位。
執行緒是程式執行的最小單位。、
執行緒共享程序資料,但也擁有自己的一部分資料:執行緒ID,一組暫存器,棧,errno,訊號遮蔽字,排程優先順序。
程序的多個執行緒共享
同一地址空間,因此Text Segment,Data Segment都是共享的,如果定義一個函式,在各執行緒中都可以訪問到,各執行緒中都以呼叫,如果定義一個全域性變數,在各執行緒中都可以訪問到,初次之外,各執行緒還共享以下程序資源和環境。
檔案描述符表
每種訊號的處理方式(SIG_IGN.SIG——DFL或者自定義的訊號處理函式)
當前工作目錄使用者id和組id
執行緒的優點:建立一個新執行緒的代價要比建立一個新程序小的多,與程序之間的切換相比,執行緒之間的切換需要作業系統做的工作要少很多。
執行緒佔用的資源比程序少很多。
能充分利用多處理器的可並行數量。
在等待慢速I/O操作結束的同時,程式可執行其他的計算任務。
計算密集型應用,為了能在多處理器系統上執行,將計算分解到多個執行緒中實現。
I/O密集型應用,為了提高效能,將I/O操作重疊。執行緒可以同時等待不同的I/O操作。
執行緒的缺點
效能損失:
一個很少被外部事件阻塞的計算密集型執行緒往往無法與共享它的執行緒共享同一個處理器。如果計算密集型執行緒的數量比可用的處理器多,那麼可能會有較大的效能損失,這裡的效能損失指的是增加了額外的同步和排程開銷,而可用的資源不變。
健壯性降低:
編寫多執行緒需要更全面更深入的考慮,在一個多執行緒程式裡,因時間分配上的細微偏差或者因共享了不該共享的變數而造成不良影響的可能性是很大的,換句話說執行緒之間是缺乏保護的。

缺乏訪問控制:程序是訪問控制的基本粒度在一個執行緒中呼叫某些OS函式會對真個程序造成影響。
程式設計難度提高:
編寫與除錯一個多執行緒程式比單執行緒程式困難得多。
int pthread_create(pthreate_t thread,const pthread_attr_t *attr,void (start_rutine)(void),void *arg)
thread:返回執行緒ID
attr:設定執行緒屬性,attr為NULL表示使用預設屬性。
start_routine:是個函式地址,執行緒啟動後要執行的函式。
arg:傳給執行緒啟動函式的引數
返回值:成功返回0,失敗返回錯誤碼。

錯誤檢查:傳統的一些函式是,成功返回0,失敗返回-1,並且對全域性變數errno賦值以指示錯誤。
pthreads函數出錯時不會設定全域性變數errno(而大部分其他POSIX函式會這樣做)而是將錯誤程式碼通過返回值返回。
pthreads同樣也提供了執行緒內errno變數,以支援其它使用errno的程式碼。對於pthreads函式的錯誤,建議通過返回值判定,因為讀取返回值要比讀取執行緒內的errno變數的開銷更小。
程序ID和執行緒ID
在Linux中,目前的執行緒實現是Native POSIX Thread Libaray,簡稱NPTL。在這種實現下,執行緒又被稱為輕量級程序,每一個使用者態的執行緒,在核心中都對應一個排程實體,也擁有自己的程序描述符。
沒有執行緒之前,一個程序對應核心裡的一個程序描述符,對應一個程序ID。但是引入執行緒概念之後,情況發生了變化,一個使用者程序下管理N個使用者態執行緒,每個執行緒作為一個獨立的排程實體在核心態都有自己的程序描述符,程序和核心的描述符一下子就變成了1:N關係,POSIX標準又要求程序內的所有執行緒呼叫getpid函式時返回相同的程序ID。
多執行緒的程序,又被稱為執行緒組,執行緒組內的每一個執行緒在核心之中都存在一個程序描述符與之對應。程序描述符結構體中的pid,表面上看對應的是程序ID,其實不然,它對應的是執行緒ID;程序描述符中的tgid,含義是Thread Group ID,該值對應的是使用者層面的程序ID
ps -L:顯示執行緒ID,執行緒組內執行緒的個數。
強調一點:執行緒和程序不一樣,程序有父程序的概念,但線上程組裡面,所有的執行緒都是對等關係。
執行緒ID及程序地址空間佈局
pthread_create函式會產生一個執行緒ID,存放在第一個引數指向的地址中。該執行緒ID和前面說的執行緒ID不是一回事。
前面講的執行緒ID屬於程序排程的範疇。因為執行緒是輕量級程序,是作業系統排程器的最小單位,所以需要一個數值來唯一表示該執行緒。
pthread_create函式產生並標記在第一個引數指向的地址中的執行緒ID中。屬於NPTL執行緒庫的範疇。執行緒庫的後續操作,就是根據該執行緒ID來操作執行緒的。
執行緒庫NPTL提供了pthread_selt函式,可以獲得執行緒 自身的ID。
pthread_t pthread_self(void);
pthread_t型別的執行緒ID,本質就是一個程序地址空間上的一個地址。
執行緒終止:從執行緒函式return。這種方法對主執行緒不適用,從main函式return相當於呼叫exit.
執行緒可以呼叫pthread_cancel終止同一程序中的另一個執行緒。
pthread_exit函式
void pthread_exit(void *value_ptr);
value_ptr:value_ptr不要指向一個區域性比變數。
無返回值,跟程序一樣,執行緒結束的時候無法返回到它的呼叫者。
需要注意,pthread_exit或者return返回的指標所指向的記憶體單元必須是全域性的或者是用malloc分配的,不能線上程函式的棧上分配,因為當其它執行緒得到這個返回指標時執行緒函式已經退出了。
int pthread_cancel(pthread_t thread);
取消一個執行中的執行緒。
成功返回0,失敗返回錯誤碼。
執行緒等待與分離
為什麼需要執行緒等待?
已經退出的執行緒,其空間沒有釋放,仍然在程序的地址空間內。
建立新的執行緒不會複用剛才退出執行緒的地址空間。
int pthread_join(pthread_t thread,void** value_ptr_
value_ptr:它指向一個指標,後者指向執行緒的返回值。
成功返回0,失敗返回錯誤碼。
呼叫該函式的執行緒將掛起等待,知道id為thread的執行緒終止,thread執行緒以不同的方法終止,通過pthread_join得到的終止狀態是不同的。
如果thread執行緒通過return 返回,value_ptr所指向的單元裡存放的是thread執行緒函式的返回值。
如果thread執行緒被別的執行緒嗲用pthread_cancel異常終止掉,value_ptr所指向的單元裡存放的是常數PTHREAD_CANCELED;
如果thread執行緒是自己呼叫pthread exit終止的,valueptr所指向的單元存放的是傳給pthread_exit的引數。
如果對thread執行緒的終止狀態不感興趣,可以傳NULL給value_ptr引數。
分離執行緒
預設情況下,新建立的執行緒是joinable的,執行緒退出後,需要對其進行pthread_join操作,否則無法釋放資源 。從而造成系統洩漏。
如果不關心執行緒的返回值,join是一種負擔,這個時候,我們可以告訴系統,當執行緒退出時,自動釋放執行緒資源。
int pthread_detach(pthread_t thread);
可以是執行緒組內其他執行緒對目標執行緒進行分離,也可以是執行緒自己分離。
pthread_detach(pthread_self());
joinable和分離是衝突的,一個執行緒不能即是joinable有時分離的。
執行緒同步與互斥
大部分情況,執行緒使用的資料都是區域性變數,變數的地址空間線上程棧空間內,這種情況,變數歸屬單個 執行緒,其他執行緒無法獲得這種變數。
但有時候,很多變數都需要線上程間共享,這樣的變數稱為共享變數,可以通過資料的共享,完成執行緒之間的互動。
多個執行緒併發的操作共享變數,會帶來一些問題。
互斥量的介面
初始化互斥量:
靜態分配:
pthread_mutex_t mutex=PTHREAD_MTEX_INITIALIZER
動態分配:int pthread_mutex_init(pthred_mutex_t restrict mutex,const pthread_mutexattr_t

restrict attr);
mutexL:要初始化的互斥量
attr:NULL;
銷燬互斥量:使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要銷燬。
不要銷燬一個已經加鎖的互斥量。
已經銷燬的互斥量,要確保後面不會有執行緒再嘗試加鎖。
int pthread_mutex_destory(pthread_mutext_t *mutex);
互斥量加鎖和解鎖
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
成功返回0,失敗返回錯誤號。
呼叫pthread_lock時,可能會遇到以下情況:
互斥量處於沒有鎖狀態,該函式會將互斥量鎖定,同時返回成功。
發起函式呼叫時,其他執行緒已經鎖定互斥量,或者存在其他執行緒同時申請互斥量,但沒有競爭到互斥量,那麼pthread_lock呼叫會陷入阻塞,等待互斥量解鎖。
條件變數:當一個執行緒互斥的訪問某個變數時,它可能發現在其它執行緒改變狀態之前,它什麼也做不了。
例如一個執行緒訪問佇列時,發現佇列為空,它只能等待,直到其它執行緒將一個節點新增到佇列的這種情況就需要用到條件變數。
int ptread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
cond:要初始化的條件變數
attr:NULL
intpthread_cond_destroy(pthread_cond_t *cond)
等到條件滿足
int pthread_cond_wait(pthread_cond_t*restrict cond,pthread_mutext_t *restrictx);
cond:要在這個條件變數上等待。
mutex:互斥量。
喚醒等待:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
為什麼pthread_cond_wait需要互斥量?
條件等待是執行緒同步間同步的一種手段,如果只有一個執行緒,條件不滿足,一直等下去都不會滿足,所以必須要有一個執行緒通過某些操作,改變共享變數,使原先不滿足的條件變得滿足,並且友好的通知等待在條件變數上的執行緒。
條件不會無緣無故的突然變得滿足了,必然會牽扯到共享資料的變化。所以一定要用互斥鎖來保護。沒有互斥鎖就無法安全的獲取和修改共享資料。
POSIX訊號量:POSIX訊號量和SystemV訊號量作用相同,都是用於同步操作,達到無衝突的訪問共享資源的目的。但可以用於執行緒間同步。
初始化訊號量

#include<semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);
pshared:0 表示執行緒間共享,非0表示程序間共享。
value:訊號量初始值
int sem_destroy(sem_t *sem)
銷燬訊號量
等待訊號量
int sem_wait(sem_t *sem);
釋出訊號量
int sem_post(sem_t *sem);
讀寫鎖:在編寫多執行緒的時候,有一種情況是十分常見的,那就是,有些公共資料修改的機會比較少,相比較改寫,它們讀的機會反而高的多。通常而言,在讀的過程中,往往伴隨著查詢操作,中間耗時很長,給這種程式碼段加鎖,會極大的降低我們程式的效率。那麼有沒有一種方法,可以專門處理這種多讀少寫的情況,那就是讀寫鎖。
注意:寫獨佔,讀共享,寫鎖優先順序高。
讀寫鎖介面:
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock,const pthread_rwlock_t *restrict attr);
銷燬:
int pthread_rwlock_destory(pthread_rwlock_t *rwlock);
加鎖和解鎖
int pthread_rwlock_rdlock(pthread_rwlock_t*rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t*rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t &rwlock);