執行緒概念:
執行緒是行程中的一條執行流程.
在linux之前學習行程的時候 ,行程就是一個pcb, 但是在現在學習執行緒的時候, 發現執行緒是行程中的一條執行流,而因為linux下執行流是通過pcb來完成的,所以理解pcb是linux下的執行流,反推得到了一個結論,linux下的一個pcb是一個執行緒,只不過人家linux下通常不談執行緒,而叫做輕量級行程. ( 有些地方認為Linux沒有真正的執行緒的說法, 執行緒實際上是一個輕量級行程. )
從另一個角度來說:
執行緒是cpu調度的基本單位, 行程是資源分配的基本單位.
執行緒之間的獨有與共享:
共享(每個執行緒相同的):
虛擬地址空間(使執行緒間可以直接通信).
信號處理方式(信號是針對行程的, 當給一個行程發送一個信號時, 所有的執行緒都能收到這個信號, 處于cpu時間片上運行的執行緒會處理這個信號, 某個地方修改了這個信號的處理方式則其他執行緒對這個信號的處理方式也跟著修改了.).
io資訊(共享檔案描述資訊, 可以操作同一檔案, 而且不同的執行緒的檔案讀寫位置是一致的, 常見的設計思路是 有一個執行緒專門負責打開檔案, 后續其他執行緒負責檔案的各種操作).
作業路徑( 比如在三級目錄下運行一個open(./text, O_CREATE,664)的test程式則text檔案生成在當前三記錄下. 如果在二級目錄下運行這個test程式: ./三級目錄名/test 運行則text檔案生成在當前的二級目錄下)等...
獨有(執行緒之間不同的):
堆疊( 區域變數存放在堆疊中, 但是把區域變數的地址給其他執行緒, 其他執行緒也能訪問到該變數,因為虛擬地址空間是同一套的.).
背景關系資料(即暫存器獨有pcb是不斷在切換運行的, 為了保存不同執行緒自己每次運行到哪了,下次時間片運行時接著運行,所以每個執行緒有自己的背景關系資料.).
errno(不同執行緒用介面操作時某個可能失敗了, 某個成功了, errno獨有則保證了不發生沖突).
信號屏蔽字(即信號阻塞集合, 執行緒信號會打斷當前操作, 為了保護某些執行緒正常運行, 就算此執行緒拿到時間片也不去處理信號. 所以執行緒之間信號阻塞集合是獨有的).
執行緒id等...
多執行緒與多行程在多任務處理中的優缺點:
多執行緒優點:
1. 執行緒間通信非常靈活(可以通過全域資料, 函式傳參, 包括行程間通信方式實作執行緒間通信)
2. 執行緒創建與銷毀成本更低(資源大多共享的, 除了獨立的資訊之外, 共享的資訊不需要重新另創建)
3. 執行緒間切換調度成本稍低(多數資料共享不需要切換調度)
多行程優點:
1. 獨立性高,穩定性強 (比如某個執行緒收到例外退出信號(或者呼叫了exit介面), 此時這個行程的所有執行緒都將退出, 信號是所有執行緒共享的, 但是如果是多行程就只退出例外的行程其他行程照常運行. 所以穩定性要求高的場景使用多行程, 比如大型的網路通信服務器, 除此之外為了便捷使用多執行緒)
共同優點:
cpu密集型程式(程式中大部分是cpu資料運算)和io密集型程式(程式中大部分是io操作)使用多行程或多執行緒的多執行流處理充分利用資源效率更高.
執行緒控制:
Linux通過執行緒庫中的各種庫函式進行執行緒控制.
執行緒創建:
int pthread_create( pthread_t* tid, pthread_attr_t* atrr, void*(*thread_routine)(void* arg), void* arg)
pthread_t* tid : 使傳入的tid實參獲取執行緒id. 后續通過這個tid操作執行緒.(執行緒的操作句柄)
pthread_attr_t* atrr : 用于設定執行緒屬性,通常置NULL.
void*(*thread_routine)(void* arg) : 執行緒入口函式,執行緒要進行的函式.
void* arg: 傳遞給執行緒入口函式的引數(若要傳多個引數, 可以組成一個結構體把結構體傳入.)
回傳值: 成功回傳0 , 失敗回傳非零值.
編譯鏈接的時候需要加: -l+庫名(為了跨平臺性也可以不加-l) 如下

實作:

運行這個程式后可以通過下面的指令觀察兩個執行緒的資訊(也可以在-L前加個l查看狀態): 5423就是主執行緒main的tid也是所有執行緒的pid. 5424就是創造的第二個執行緒的tid.

執行緒終止:
執行緒進行的函式運行結束了, 這個執行緒就終止(退出)了.
1. 執行緒入口函式 return ( main主函式return則退出了行程, 所有執行緒都終止,和下面不同 )
2. void pthread_exit(void* retval) 介面 , 沒有回傳值, 通過傳入的引數獲取執行緒退出的回傳值.(不需要則置NULL). 哪個執行緒呼叫了這個介面該執行緒就退出 (和return不同的是, 如果執行緒入口函式呼叫了另一個函式, 那個函式中有這個exit介面則執行緒直接退出了. 如果沒有,運行完呼叫函式回傳到入口函式再運行到入口函式的return才退出.)
3.int pthread_cancel(pthread_t tid) 任意位置退出指定執行緒. 主執行緒呼叫pthread_cancel(pthread_self() )函式, 或pthread_exit(NULL) 則主執行緒的狀態變更成為Z, 其他執行緒不受影響
執行緒等待:
默認情況下, 一個執行緒退出如果不等待也會造成資源泄露, 所以需要等待指定執行緒的退出, 獲取這個執行緒的退出回傳值, 從而釋放資源. 有時候不僅僅是為了防止資源泄漏等待, 是必須等到某個執行緒處理完得到結果或者是必須等某個執行緒退出才能往下運行時等待.
執行緒等待介面: int pthread_join(pthread_t tid , void** retval) 是個阻塞等待介面.
tid: 等待指定的執行緒退出.
retval: 用于接收執行緒退出回傳值. 通常定義一個void* retval 然后傳入 &retval 作實參.(如果定義void** retval直接傳retval會發成解參考野指標的問題. )不需要則置NULL


但是,當我們不關心一個執行緒的回傳值的時候,又不需要等待現成推出才能往下運行,這時候等待會導致性能降低, 在這種場景之下,等待就不合適了,但是不等待又會資源泄露基于這個需求就有了執行緒分離
執行緒分離:
執行緒有很多屬性, 其中有一個叫做分離屬性, 分離屬性默認值-JOINABLE, 表示執行緒退出之后不會自動釋放資源 , 需要被等待, 如果將執行緒的分離屬性設定為其他值-DETACH,這時候則執行緒退出后之后將不需要被等待,而是直接釋放資源, 因為執行緒一旦設定了分離屬性,則退出后自動釋放資源,則等待將毫無意義,所以設定了分離屬性的執行緒是不能被等待. 需要等待的執行緒則不會設定執行緒分離
int pthread_detach( pthread_t tid) 介面將指定執行緒分離屬性設定為detach. ( 通常在一個執行緒介面自己內部剛開始第一行就使用pthread_detach( pthread_self() ))
不想等待某個執行緒且不需要它的回傳值則將這個執行緒分離.
執行緒安全(的問題):
多執行緒同時修改同一個臨界資源可能會造成資料的二義性. 所以需要實作執行緒安全保證多執行緒對同一個臨界資源的的訪問操作是安全的.
實作執行緒安全的方法: 同步與互斥.
互斥(通過 互斥鎖 實作): 保證執行流在同一時間對臨界資源的唯一訪問.
同步(通過 條件變數, 信號量 實作): 通過一些規則(判斷條件)實作執行緒對資源獲取的秩序合理.
*互斥鎖:
互斥鎖的本質是一個 0/1 計數器, 主要用于標記臨界資源的訪問狀態. 0不可訪問,1可訪問.
互斥鎖操作: 訪問資源之前加鎖(加不上鎖則阻塞,因為資源還沒有解鎖), 訪問資源完畢則解鎖.
互斥鎖實作互斥, 本質上自己也是個臨界資源
同一個資源所有執行緒訪問的時候加的是同一把鎖. 不同的鎖則加了沒有意義.
為了保證互斥鎖自身的操作是安全的, 互斥鎖內部的操作是原子操作.
介面流程:
在互斥鎖變數mutex加解鎖之間的代碼是受保護的安全是臨界區.

火車站買票示例:


上述示例中出問題的原因在于, 沒有互斥鎖保護臨界資源就會:在搶票操作的1ms時間內, 時間片給到另外的黃牛, 此時第一個進入搶最后一場票的黃牛還沒完成搶票操作,票數還為1, 第二第三個黃牛運行一看還有票就也進行了搶票操作. 就導致了票數不正常的情況. 則此時我們需要對票數這個資源進行互斥鎖操作:


上述程式中, 發現都是同一個黃牛在搶票, 這是因為互斥鎖只能保證安全操作,無法保證合理.
因為在加完鎖之后, 第一個進入搶票的黃牛搶完票再解鎖, 此時因為時間還在他手上他又馬上運行到加鎖搶票的程序. 然后如此往復其他黃牛每次有時間片想加鎖都失敗然后阻塞了.解鎖的時候時間片還在原來搶票的黃牛手上,就導致了搶票不合理. (只是互斥的不合理,不是下面說的死鎖哈) 下面的同學吃飯初始做飯加入了條件變數的同步操作就可以合理的不同的同學吃飯不同的廚師做飯.
*死鎖:
程式流程流程無法繼續運行, 卡死的情況叫死鎖.
產生原因: 由于對鎖資源爭搶順序不當所致.
導致死鎖的四個必要條件 ( 四個條件都發生則產生死鎖 ):
1.互斥條件: 一個執行緒加了鎖, 別人不能再加.
2.不可剝奪條件: 我加的鎖別人不能解.
(前兩個條件是是加互斥鎖之后必然的,所以看是否死鎖得看后兩個是否發生)
3.請求與保持條件: 加A鎖后請求B鎖,B鎖請求不到(因為B鎖已被加鎖)而不釋放A鎖.
4.環路等待條件: 加A鎖請求B鎖,對方已經加B鎖請求A鎖.
綜上理解: 因為規則是一個資源只能被一個執行緒(1號執行緒)加鎖, 且自己加的鎖別人不能解. 然后1號執行緒加了一個資源A的鎖后想給另一個資源B加自己的鎖, 而另一個資源B已經被另一個執行緒(2號執行緒)加了2號自己的鎖 1號執行緒就加不了也解不了, 而且2號執行緒同時也想給1號執行緒加了鎖的A資源加鎖. 因為1號執行緒得不到B鎖就不釋放A鎖二號就得不到A鎖, 2號執行緒得不到A鎖也不釋放B鎖. 就導致了死鎖( 例子: 哲學家吃飯問題, 哲學家坐一圈圓桌吃飯, 每個哲學家只有一只筷子, 每個都想要旁邊的人的另一只筷子 , 而每個人得不到另一只筷子吃不到飯就不把自己的筷子給別人導致了死鎖.所以條件1,2理解就是一個筷子只能同時被一個人用, 且不能搶被人的筷子)
對同一資源加(解)鎖順序不一致導致了環路等待條件, 阻塞加鎖導致了請求與保持條件. 所以預防死鎖就得保證加解鎖順序一致, 使用非阻塞加鎖.
避免死鎖方案: 銀行家演算法, 銀行家演算法的思想在于將系統運行分為兩種狀態:安全/非安全,有可能出現風險的都屬于非安全, 安全狀態則系統中一定無死鎖行程(思想:查看資源請求表,哪個執行緒要請求哪個鎖,根據所有資源表和已分配資源表判斷,這個鎖分配給執行緒是否有可能造成環路等待(可能造成則不安全),不安全則不予分配.) 等演算法...
死鎖的處理辦法:
鴕鳥策略 對可能出現的問題采取無視態度,前提是出現概率很低
預防策略 破壞死鎖產生的必要條件
避免策略 銀行家演算法,分配資源前進行風險判斷,避免風險的發生
檢測與解除死鎖 分配資源時不采取措施,但是必須提供死鎖的檢測與解除手段
*同步:
概念: 通過一些條件判斷保證執行流對資源獲取的秩序合理.
即a執行緒達到某些條件時喚醒b執行緒. 然后自己再陷入加鎖阻塞狀態,等b執行緒達到喚醒自己的條件又b又喚醒a執行緒,如此往復,就實作了同步.
實作方式: 條件變數, 信號量. (信號量也能實作互斥)
*條件變數:
例子:

除了等待join直接傳mutex.
加解鎖, 初始化, 等待, 喚醒, 銷毀傳 &mutex, &cond, 初始化和等待加個NULL.
條件變數和互斥鎖實作同步與互斥的學生吃飯廚師做飯問題:



*所以注意事項就是:
1.是否滿足需要阻塞條件的判斷應該使用回圈操作!!!!
2.多種角色執行緒等待應該分開等待,分開喚醒防止喚醒角色錯誤多種角色定義多個條件變數.
設計模式 (多執行緒的應用): 生產者與消費者模型
設計模式是大佬們針對典型應用場景設計的解決方案. 生產者與消費者模型就是針對有大量資料產生及處理的場景的設計模式, 下面說的單例模式也是一種設計模式(針對的是一個類只能實體化一個物件,提供一個訪問介面,一個資源在記憶體中只能有一份的場景), 以后遇到某些典型的場景就可以使用大佬們搞好的特定設計模式解.
生產者與消費者模型特點: 1. 解耦合(生產和處理分開,生產執行緒負責生產處理執行緒負責處理, 處理需更多時間和資源, 多創建幾個處理執行緒) 2. 支持忙先不均(生產執行緒與處理執行緒并不直接互動, 生產執行緒生產的要處理的資料先放入一個資料緩沖佇列, 處理執行緒空閑的話就查看這個緩沖任務佇列, 有任務則取出處理.) 3.支持并發(多個生產處理執行緒訪問同一個任務佇列, 所以這個資料緩沖佇列必須保證執行緒安全同一時間只有一個執行緒對佇列操作.)
條件變數和互斥鎖實作生產者與消費者模型: 兩種角色的執行緒負責入隊(生產)和出隊(處理), 和一個執行緒提供入隊出隊的安全的佇列.



注意: 運行時出現列印的入隊 (出隊) 資料個數比定義的最大的資料個數MAXQ多的原因是因為入對資料 列印, 出隊資料 列印這兩個地方的兩步操作不是原子操作, 運行了_push之后資料最大了, 然后執行緒阻塞, 時間片輪轉到別的執行緒, 等待下一次時間片搶到空余位置插入資料再列印然后繼續運行插入資料,這就是列印多出資料的原因, 列印多了不代表佇列中的資料多了. 如果將生產者或消費者中的某一角色執行緒個數比另一角色執行緒個數多好多的時候就會出現一對一如隊即出隊互動了的假象
*信號量(POSIX):
posix標準信號量:
計數器用于執行緒可以是區域變數通過傳參使用同一個,或者使用全域變數
計數器用于行程間,這個計數器是通過共享記憶體實作的
systemV標準信號量: linux內核提供的一個計數器
本質: 一個計數器, 用于實作行程或執行緒之間的同步與互斥.

p操作: 計數減一, 且判斷技術是否大于等于0 , 大于等于0則回傳, 小于0則阻塞.
v操作: 計數加一, 且喚醒一個阻塞的行程或執行緒 (其實sem_post喚醒多個阻塞行程或執行緒,但是真正獲取到資源的只有一個, 其他的沒有資源又會陷入阻塞).
信號量實作同步: 通過隊資源數量進行計數, 獲取資源之前進行p操作, 產生資源之后進行v操作. 通過這種方式實作對資源的合理獲取.
信號量實作互斥: 計數器初始值為1 ,訪問資源前進行p操作, 訪問完畢進行v操作, 實作類似加解鎖的操作.(真正使用中不需要用信號實作鎖, 都是用定義好的mutex互斥鎖)
信號量實作的生產者與消費者模型:



這里運行結果和上面條件變數與互斥鎖實作的一樣, 列印的資料個數有誤,原因也是因為_push (_pop)操作和列印操作不是原子操作.
條件變數與信號量實作同步上的區別:
1.本質上的不同,信號量是個計數器,條件變數沒有計數器,因此條件變數的資源訪問合理性需要用戶自己進行,但是信號量可以通過自身計數完成,
⒉.條件變數需要搭配互斥鎖一起使用,而信號量不需要
其他一些鎖:
*執行緒池的簡單實作:
執行緒池其實就是一堆(一個或多個)執行緒進行任務處理. 針對有大量任務需要處理的場景.
上面的生產者消費者模型思想其實就是多執行緒進行任務處理的思想 , 執行緒池則可以說是多執行緒任務處理的具體應用. 所以類似的, 執行緒池的實作思想就是一堆創建好的執行緒和執行緒安全的任務佇列. 有任務進入執行緒池中,就會分配一個執行緒處理. 執行緒池和來一個任務就創建一個執行緒處理比的優點是: 1. 節省了任務處理程序中執行緒創建和銷毀的時間成本 2. 執行緒池中的執行緒和任務節點數量都有最大限制, 避免資源耗盡風險.
為了降低執行緒池的耦合度, 在給出任務進入執行緒池時應同時給出任務的處理方法.( 通過函式指標 ),所以給進任務佇列里的任務就不只是要處理的資料了, 得加上資料對應的解決方法, 兩者合并為一個taskfun類傳入任務佇列里. 執行緒池負責將任務入隊和從隊中取出任務并處理.
實作:





*執行緒安全的單例模式:
單例模式也是一種設計模式, 針對一個類只能實體化一個物件, 提供一個訪問介面的場景(一個資源在記憶體中只能有一份的場景)
目的是: 1.節省空間 2.防止資料二義性
兩種實作方式:
餓漢(資源全部提前加載完畢, 用的時候可以直接用, 以空間換時間)
template<class T>
class singleton{
public:
//單例模式只有一個類外能訪問的介面,其他構造,拷貝構造,賦值多載都是私有的
steatic T* Getlenstance(){
return &data;
}
private:
static T data;//靜態成員屬于全域, 程式運行前就加載好了.
singleton(){} //建構式私有化, 無法在類外實體化物件.
static singleton _mysingleton;//類內初始化.
};
懶漢(資源用的時候才加載,不用不需要加載. 延遲加載,用的地方更多)
template<class T>
class singleton{
public:
//volatile關鍵字防止編譯器過度優化
//類外初始化靜態成員時:T* singleton::data=NULL
//不使用volatile被編譯器優化后會一直使用暫存器中的NULL.
volatile static T* getlenstance(){
if(data==NULL){ //加鎖前二次檢查,提高效率.
_mutex.lock();//多執行緒可能同時訪問,為了執行緒安全加鎖
if(data==NULL){
data=new T();//申請呼叫時才加載
_mutex.unlock();
}
}
return data;
}
private:
volatile static T* data; //靜態指標, 申請使用時才加載變數.
static std::mutex _mutex;
singleton(){} //建構式初始化
};
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/374663.html
標籤:其他
下一篇:linux如何手工配置ip地址
