多執行緒應用初探(一)----(概念,安全)

派二星發表於2019-04-08

執行緒是程式的基本執行單元,一個程式的所有任務都線上程中執行 程式要想執行任務,必須得有執行緒,程式至少要有一條執行緒 程式啟動會預設開啟一條執行緒,這條執行緒被稱為主執行緒或 UI 執行緒 程式是指在系統中正在執行的一個應用程式 每個程式之間是獨立的,每個程式均執行在其專用的且受保護的記憶體

執行緒與程式之間的關係

地址空間:同一程式的執行緒共享本程式的地址空間,而程式之間則是獨立的地址空間。
資源擁有:同一程式內的執行緒共享本程式的資源如記憶體、I/O、cpu等,但是程式之間的資源是獨立的。
一個程式崩潰後,在保護模式下不會對其他程式產生影響,但是一個執行緒崩潰整個程式都死掉。所以多程式要比多執行緒健壯。
程式切換時,消耗的資源大,效率高。所以涉及到頻繁的切換時,使用執行緒要好於程式。同樣如果要求同時進行並且又要共享某些變數的併發操作,只能用執行緒不能用程式
執行過程:每個獨立的程式有一個程式執行的入口、順序執行序列和程式入口。但是執行緒不能獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制。
執行緒是處理器排程的基本單位,但是程式不是

多執行緒

優點

  • 能適當提高程式的執行效率
  • 能適當提高資源的利用率(CPU,記憶體)
  • 執行緒上的任務執行完成後,執行緒會自動銷燬

缺點

  • 開啟執行緒需要佔用一定的記憶體空間(預設情況下,每一個執行緒都佔 512 KB)
  • 如果開啟大量的執行緒,會佔用大量的記憶體空間,降低程式的效能
  • 執行緒越多,CPU 在呼叫執行緒上的開銷就越大 * 程式設計更加複雜,比如執行緒間的通訊、多執行緒的資料共享

原理

多執行緒在單位時間片內快速在各個執行緒之間切換

執行緒生命週期

start ==》 Runable ==》 Runing ==》 blocked(呼叫sleep方法等待同步鎖可排程執行緒池裡移出) ==》 dead
新建 ==》 就緒 ==》 執行(cpu排程當前執行緒) ==> 堵塞 ==》任務完成強制退出

執行緒和RunLoop的關係

  1. RunLoop與執行緒是一一對應的,一個Runloop對應一個核心執行緒,Runloop是可以巢狀的,他們的關係保持在一個全域性的字典裡
  2. RunLoop管理執行緒
  3. 第一獲取時被建立,執行緒結束時被銷燬
  4. 對於主執行緒Runloop預設建立好,Runloop在子執行緒懶載入使用時才建立

多執行緒安全 ---- 鎖和效能

在多執行緒操作過程中,往往一個資料同時被多個執行緒讀寫,在這種情況下,如果沒有相應的機制對資料進行保護,就很可能會發生資料汙染的的問題,給程式造成各種難以重現的潛在bug。

多執行緒安全中相關術語及概念(假設操作的是資料庫):

(1)髒讀

指當一個事務正在訪問資料,並且對資料進行了修改,而這種修改還沒有提交到資料庫中。這時,另外一個事務也訪問這個資料,然後使用了這個資料。因為這個資料是還沒有提交的資料,那麼另外一個事務讀到的這個資料是髒資料,依據髒資料所做的操作可能是不正確的。

(2)不可重複讀

指在一個事務內,多次讀同一資料。在這個事務還沒有結束時,另外一個事務也訪問該同一資料。那麼,在第一個事務中的兩次讀資料之間,由於第二個事務的修改,那麼第一個事務兩次讀到的的資料可能是不一樣的。這樣就發生了在一個事務內兩次讀到的資料是不一樣的,因此稱為是不可重複讀。

(3)幻覺讀

指當事務不是獨立執行時發生的一種現象,例如第一個事務對一個表中的資料進行了修改,這種修改涉及到表中的全部資料行。同時,第二個事務也修改這個表中的資料,這種修改是向表中插入一行新資料。那麼,以後就會發生操作第一個事務的使用者發現表中還有沒有修改的資料行,就好象發生了幻覺一樣。例如: 目前工資為5000的員工有10人,事務A讀取所有工資為5000的人數為10人。此時,事務B插入一條工資也為5000的記錄。這時,事務A再次讀取工資為5000的員工,記錄為11人。此時產生了幻讀。


執行緒不安全:就是不提供資料訪問保護,有可能出現多個執行緒先後更改資料造成所得到的資料是髒資料。當多個執行緒訪問同一塊資源時,很容易引發資料錯亂和資料安全問題。
執行緒安全:簡單來說就是多個執行緒同時對共享資源進行訪問時,採用了加鎖機制,當一個執行緒訪問共享資源,對該資源進行保護,其他執行緒不能進行訪問直到該執行緒讀取完,其他執行緒才可使用。

多執行緒的鎖

互斥鎖

@synchronized(id anObject)

- (void)myMethod:(id)anObj
{
    @synchronized(anObj)
    {
        //do something here
    }
}
複製程式碼

當一個執行緒在對某個資源進行讀取時,另一條想要讀取的執行緒將要進入等待狀態,當該執行緒放問完畢時,等待的執行緒在對其進行訪問,atomic就是對屬性的Set方法加互斥鎖,但這並不能完全保證執行緒安全,因為他僅僅對set方法進行了加鎖

NSLock

NSLock物件實現了NSLocking protocol,包含幾個方法: lock——加鎖 unlock——解鎖 tryLock——嘗試加鎖,如果失敗了,並不會阻塞執行緒,只是立即返回NO lockBeforeDate:——在指定的date之前暫時阻塞執行緒(如果沒有獲取鎖的話),如果到期還沒有獲取鎖,則執行緒被喚醒,函式立即返回NO。 比如:

NSLock *theLock = [[NSLock alloc] init]; 
if ([theLock lock]) 
{
   //do something here
   [theLock unlock]; 
}
複製程式碼

遞迴鎖

NSRecursiveLock

多次呼叫不會阻塞已獲取該鎖的執行緒。

NSRecursiveLock *rcsLock = [[NSRecursiveLock alloc] init]; 

void recursiveLockTest(int value) 
{ 
  [rcsLock lock]; 
  if (value != 0) 
  { 
    --value; 
    recursiveLockTest(value); 
  }
  [rcsLock unlock]; 
} 

recursiveLockTest(5)
複製程式碼

上面如果直接使用NSLock就會造成死鎖。NSRecursiveLock類定義的鎖可以在同一執行緒多次lock,而不會造成死鎖。遞迴鎖會跟蹤它被多少次lock。每次成功的lock都必須平衡呼叫unlock操作。只有所有的鎖住和解鎖操作都平衡的時候,鎖才真正被釋放給其他執行緒獲得。

NSConditionLock 條件鎖

有時一把只會lock和unlock的鎖未必就能完全滿足我們的使用。因為普通的鎖只能關心鎖與不鎖,而不在乎用什麼鑰匙才能開鎖,而我們在處理資源共享的時候,多數情況是隻有滿足一定條件的情況下才能開啟這把鎖:

//主執行緒中
NSConditionLock *theLock = [[NSConditionLock alloc] init];

//執行緒1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    for (int i=0;i<=2;i++)
    {
        [theLock lock];
        NSLog(@"thread1:%d",i);
        sleep(2);
        [theLock unlockWithCondition:i];
    }
});

//執行緒2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [theLock lockWhenCondition:2];
    NSLog(@"thread2");
    [theLock unlock];
});
複製程式碼

線上程1中的加鎖使用了lock,是不需要條件的,所以順利的就鎖住了。但在unlock的使用了一個整型的條件,它可以開啟其它執行緒中正在等待這把鑰匙的臨界地,而執行緒2則需要一把被標識為2的鑰匙,所以當執行緒1迴圈到最後一次的時候,才最終開啟了執行緒2中的阻塞。但即便如此,NSConditionLock也跟其它的鎖一樣,是需要lock與unlock對應的,只是lock、lockWhenCondition:與unlock,unlockWithCondition:是可以隨意組合的.

分佈鎖:NSDistributedLock

以上所有的鎖都是在解決多執行緒之間的衝突,但如果遇上多個程式或多個程式之間需要構建互斥的情景該怎麼辦呢?這個時候我們就需要使用到NSDistributedLock了,從它的類名就知道這是一個分散式的Lock,NSDistributedLock的實現是通過檔案系統的,所以使用它才可以有效的實現不同程式之間的互斥,但NSDistributedLock並非繼承於NSLock,它沒有lock方法,它只實現了tryLock,unlock,breakLock,所以如果需要lock的話,你就必須自己實現一個tryLock的輪詢。

GCD中訊號量:dispatch_semaphore

假設現在系統有兩個空閒資源可以被利用,但同一時間卻有三個執行緒要進行訪問,這種情況下,該如何處理呢?這裡,我們就可以方便的利用訊號量來解決這個問題。同樣我們也可以用它來構建一把”鎖”(從本質上講,訊號量與鎖是有區別的,具體的請自行查閱資料)。

訊號量:就是一種可用來控制訪問資源的數量的標識。設定了一個訊號量,線上程訪問之前,加上訊號量的處理,則可告知系統按照我們指定的訊號量數量來執行多個執行緒。

在GCD中有三個函式是semaphore的操作: dispatch_semaphore_create 建立一個semaphore dispatch_semaphore_signal 傳送一個訊號 dispatch_semaphore_wait 等待訊號

dispatch_semaphore_create函式有一個整形的引數,我們可以理解為訊號的總量,dispatch_semaphore_signal是傳送一個訊號,自然會讓訊號總量+1,dispatch_semaphore_wait等待訊號,當訊號總量少於0的時候就會一直等待,否則就可以正常的執行,並讓訊號總量-1,根據這樣的原理,我們便可以快速的建立一個併發控制來同步任務和有限資源訪問控制。

GCD中“柵欄函式”:dispatch_barrier_async

dispatch_barrier_async函式的作用與barrier的意思相同,在程式管理中起到一個柵欄的作用,它等待所有位於barrier函式之前的操作執行完畢後執行,並且在barrier函式執行之後,barrier函式之後的操作才會得到執行,該函式需要同dispatch_queue_create函式生成的concurrent Dispatch Queue佇列一起使用。

自旋鎖

它的特點是線上程等待時會一直輪詢,處於忙等狀態。自旋鎖由此得名。 自旋鎖看起來是比較耗費cpu的,然而在互斥臨界區計算量較小的場景下,它的效率遠高於其它的鎖。因為它是一直處於running狀態,減少了執行緒切換上下文的消耗。OSSpinLock是一種自旋鎖,但是這種鎖是不安全的 關於 OSSpinLock 不再安全,原因就在於優先順序反轉問題。

優先順序翻轉:一個高優先順序任務間接被一個低優先順序任務所搶先(preemtped),使得兩個任務的相對優先順序被倒置。 這往往出現在一個高優先順序任務等待訪問一個被低優先順序任務正在使用的臨界資源,從而阻塞了高優先順序任務;同時,該低優先順序任務被一個次高優先順序的任務所搶先,從而無法及時地釋放該臨界資源。這種情況下,該次高優先順序任務獲得執行權。
當一個高優先順序的任務需要一個資源,但是此時這個資源正在被低優先順序任務所佔有,這種情況造成高優先順序任務需要等待低優先順序任務完成之後才能執行,但是次高階別任務並不需要資源,所以他可以在低優先順序任務之前執行,所以間接的次優先順序任務就在高優先順序任務之前被執行了。使得優先順序被倒置了。假設高優先順序任務等待資源時不是堵塞等待,而是忙著迴圈,則可能永遠無法獲得資源,因為低優先順序任務沒有執行任務的時間片,進而無法釋放資源,高優先順序任務也永遠不會推進。

效能對比

本節參考文章imlifengfeng

優先順序反轉解決的辦法

  1. 優先順序繼承,故名思義,是將佔有鎖的執行緒優先順序,繼承等待該鎖的執行緒高優先順序,如果存在多個執行緒等待,就取其中之一最高的優先順序繼承。
  2. 優先順序天花板,則是直接設定優先順序上限,給臨界區一個最高優先順序,進入臨界區的程式都將獲得這個高優先順序。
  3. 禁止中斷的特點,在於任務只存在兩種優先順序:可被搶佔的 / 禁止中斷的 。 前者為一般任務執行時的優先順序,後者為進入臨界區的優先順序。通過禁止中斷來保護臨界區,沒有其它第三種的優先順序,也就不可能發生反轉了。

相關文章