多線程應用初探(一)----(概念,安全)

線程是進程的基本執行單元,一個進程的全部任務都在線程中執行 進程要想執行任務,必須得有線程,進程至少要有一條線程 程序啓動會默認開啓一條線程,這條線程被稱爲主線程或 UI 線程 進程是指在系統中正在運行的一個應用程序 每一個進程之間是獨立的,每一個進程均運行在其專用的且受保護的內存git

線程與進程之間的關係

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

多線程

優勢

  • 能適當提升程序的執行效率
  • 能適當提升資源的利用率(CPU,內存)
  • 線程上的任務執行完成後,線程會自動銷燬

缺點

  • 開啓線程須要佔用必定的內存空間(默認狀況下,每個線程都佔 512 KB)
  • 若是開啓大量的線程,會佔用大量的內存空間,下降程序的性能
  • 線程越多,CPU 在調用線程上的開銷就越大 * 程序設計更加複雜,好比線程間的通訊、多線程的數據共享

原理

多線程在單位時間片內快速在各個線程之間切換數據庫

線程生命週期

start ==》 Runable ==》 Runing ==》 blocked(調用sleep方法等待同步鎖可調度線程池裏移出) ==》 dead
新建 ==》 就緒 ==》 運行(cpu調度當前線程) ==> 堵塞 ==》任務完成強制退出安全

線程和RunLoop的關係

  1. RunLoop與線程是一一對應的,一個Runloop對應一個核心線程,Runloop是能夠嵌套的,他們的關係保持在一個全局的字典裏
  2. RunLoop管理線程
  3. 第一獲取時被建立,線程結束時被銷燬
  4. 對於主線程Runloop默認建立好,Runloop在子線程懶加載使用時才建立

多線程安全 ---- 鎖和性能

在多線程操做過程當中,每每一個數據同時被多個線程讀寫,在這種狀況下,若是沒有相應的機制對數據進行保護,就極可能會發生數據污染的的問題,給程序形成各類難以重現的潛在bug。bash

多線程安全中相關術語及概念(假設操做的是數據庫):多線程

(1)髒讀併發

指當一個事務正在訪問數據,而且對數據進行了修改,而這種修改尚未提交到數據庫中。這時,另一個事務也訪問這個數據,而後使用了這個數據。由於這個數據是尚未提交的數據,那麼另一個事務讀到的這個數據是髒數據,依據髒數據所作的操做多是不正確的。async

(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. 禁止中斷的特色,在於任務只存在兩種優先級:可被搶佔的 / 禁止中斷的 。 前者爲通常任務運行時的優先級,後者爲進入臨界區的優先級。經過禁止中斷來保護臨界區,沒有其它第三種的優先級,也就不可能發生反轉了。
相關文章
相關標籤/搜索