• iOS "鎖"的作用及使用方式

    小編:艷芬 412閱讀 2020.10.26

    • 鎖是什么?
    • 為什么要有鎖?
    • 鎖的分類問題
    • 為什么 OSSpinLock 不安全?
    • 解決自旋鎖不安全問題有幾種方式
    • 為什么換用其它的鎖,可以解決 OSSpinLock 的問題?
    • 自旋鎖和互斥鎖的關系是平行對立的嗎?
    • 信號量和互斥量的關系
    • 信號量和條件變量的區別
    鎖是什么

    鎖 -- 是保證線程安全常見的同步工具。鎖是一種非強制的機制,每一個線程在訪問數據或者資源前,要先獲取(Acquire) 鎖,并在訪問結束之后釋放(Release)鎖。如果鎖已經被占用,其它試圖獲取鎖的線程會等待,直到鎖重新可用。

    為什么要有鎖?

    鎖是用來保護線程安全的工具。

    可以試想一下,多線程編程時,沒有鎖的情況 -- 也就是線程不安全。

    當多個線程同時對一塊內存發生讀和寫的操作,可能出現意料之外的結果:

    程序執行的順序會被打亂,可能造成提前釋放一個變量,計算結果錯誤等情況。

    所以我們需要將線程不安全的代碼 “鎖” 起來。保證一段代碼或者多段代碼操作的原子性,保證多個線程對同一個數據的訪問?同步 (Synchronization)。

    屬性設置 atomic

    上面提到了原子性,我馬上想到了屬性關鍵字里, atomic 的作用。

    設置 atomic 之后,默認生成的 getter 和 setter 方法執行是原子的。

    但是它只保證了自身的讀/寫操作,卻不能說是線程安全。

    如下情況:

    //thread A
    for (int i = 0; i < 100000; i ++) {
    if (i % 2 == 0) {
        self.arr = @[@"1", @"2", @"3"];
    }else {
        self.arr = @[@"1"];
    }
    NSLog(@"Thread A: %@\n", self.arr);
    }
    
    //thread B
    if (self.arr.count >= 2) {
        NSString* str = [self.arr objectAtIndex:1];
    }

    就算在?thread B?中針對 arr 數組進行了大小判斷,但是仍然可能在?objectAtIndex:?操作時被改變數組長度,導致出錯。這種情況聲明為 atomic 也沒有用。

    而解決方式,就是進行加鎖。

    需要注意的是,讀/寫的操作都需要加鎖,不僅僅是對一段代碼加鎖。

    鎖的分類

    鎖的分類方式,可以根據鎖的狀態,鎖的特性等進行不同的分類,很多鎖之間其實并不是并列的關系,而是一種鎖下的不同實現。

    自旋鎖和互斥鎖的關系

    很多談論鎖的文章,都會提到互斥鎖,自旋鎖。很少有提到它們的關系,其實自旋鎖,也是互斥鎖的一種實現,而?spin lock和?mutex?兩者都是為了解決某項資源的互斥使用,在任何時刻只能有一個保持者。

    區別在于?spin lock和?mutex?調度機制上有所不同。

    OSSpinLock

    OSSpinLock 是一種自旋鎖。它的特點是在線程等待時會一直輪詢,處于忙等狀態。自旋鎖由此得名。

    自旋鎖看起來是比較耗費 cpu 的,然而在互斥臨界區計算量較小的場景下,它的效率遠高于其它的鎖。

    因為它是一直處于 running 狀態,減少了線程切換上下文的消耗。

    為什么 OSSpinLock 不再安全?

    關于 OSSpinLock 不再安全,原因就在于優先級反轉問題。

    優先級反轉(Priority Inversion)

    什么情況叫做優先級反轉?

    wikipedia 上是這么定義的:

    優先級倒置,又稱優先級反轉、優先級逆轉、優先級翻轉,是一種不希望發生的任務調度狀態。在該種狀態下,一個高優先級任務間接被一個低優先級任務所搶先(preemtped),使得兩個任務的相對優先級被倒置。 這往往出現在一個高優先級任務等待訪問一個被低優先級任務正在使用的臨界資源,從而阻塞了高優先級任務;同時,該低優先級任務被一個次高優先級的任務所搶先,從而無法及時地釋放該臨界資源。這種情況下,該次高優先級任務獲得執行權。

    再消化一下

    有:高優先級任務A / 次高優先級任務B / 低優先級任務C / 資源Z 。A 等待 C 執行后的 Z,而 B 并不需要 Z,搶先獲得時間片執行。C 由于沒有時間片,無法執行(優先級相對沒有B高)。 這種情況造成 A 在C 之后執行,C在B之后,間接的高優先級A在次高優先級任務B 之后執行, 使得優先級被倒置了。(假設: A 等待資源時不是阻塞等待,而是忙循環,則可能永遠無法獲得資源。此時 C 無法與 A 爭奪 CPU 時間,從而 C 無法執行,進而無法釋放資源。造成的后果,就是 A 無法獲得 Z 而繼續推進。)

    而 OSSpinLock 忙等的機制,就可能造成高優先級一直 running ,占用 cpu 時間片。而低優先級任務無法搶占時間片,變成遲遲完不成,不釋放鎖的情況。

    優先級反轉的解決方案

    關于優先級反轉一般有以下三種解決方案

    優先級繼承

    優先級繼承,故名思義,是將占有鎖的線程優先級,繼承等待該鎖的線程高優先級,如果存在多個線程等待,就取其中之一最高的優先級繼承。

    優先級天花板

    優先級天花板,則是直接設置優先級上限,給臨界區一個最高優先級,進入臨界區的進程都將獲得這個高優先級。

    如果其他試圖進入臨界區的進程的優先級,都低于這個最高優先級,那么優先級反轉就不會發生。

    禁止中斷

    禁止中斷的特點,在于任務只存在兩種優先級:可被搶占的 / 禁止中斷的 。

    前者為一般任務運行時的優先級,后者為進入臨界區的優先級。

    通過禁止中斷來保護臨界區,沒有其它第三種的優先級,也就不可能發生反轉了。

    為什么使用其它的鎖,可以解決優先級反轉?

    我們看到很多本來使用 OSSpinLock 的知名項目,都改用了其它方式替代,比如 pthread_mutex 和 dispatch_semaphore 。

    那為什么其它的鎖,就不會有優先級反轉的問題呢?如果按照上面的想法,其它鎖也可能出現優先級反轉。

    原因在于,其它鎖出現優先級反轉后,高優先級的任務不會忙等。因為處于等待狀態的高優先級任務,沒有占用時間片,所以低優先級任務一般都能進行下去,從而釋放掉鎖。

    線程調度

    為了幫助理解,要提一下有關線程調度的概念。

    無論多核心還是單核,我們的線程運行總是 "并發" 的。

    當 cpu 數量大于等于線程數量,這個時候是真正并發,可以多個線程同時執行計算。

    當 cpu 數量小于線程數量,總有一個 cpu 會運行多個線程,這時候"并發"就是一種模擬出來的狀態。操作系統通過不斷的切換線程,每個線程執行一小段時間,讓多個線程看起來就像在同時運行。這種行為就稱為?"線程調度(Thread Schedule)"。

    線程狀態

    在線程調度中,線程至少擁有三種狀態 :?運行(Running),就緒(Ready),等待(Waiting)。

    處于?Running的線程擁有的執行時間,稱為 時間片(Time Slice),時間片 用完時,進入Ready狀態。如果在Running狀態,時間片沒有用完,就開始等待某一個事件(通常是 IO 或 同步 ),則進入Waiting狀態。

    如果有線程從Running狀態離開,調度系統就會選擇一個Ready的線程進入?Running?狀態。而Waiting的線程等待的事件完成后,就會進入Ready狀態。

    dispatch_semaphore

    dispatch_semaphore?是 GCD 中同步的一種方式,與他相關的只有三個函數,一個是創建信號量,一個是等待信號,一個是發送信號。

    信號量機制

    信號量中,二元信號量,是一種最簡單的鎖。只有兩種狀態,占用和非占用。二元信號量適合唯一一個線程獨占訪問的資源。而多元信號量簡稱 信號量(Semaphore)。

    信號量和互斥量的區別

    信號量是允許并發訪問的,也就是說,允許多個線程同時執行多個任務。信號量可以由一個線程獲取,然后由不同的線程釋放。

    互斥量只允許一個線程同時執行一個任務。也就是同一個程獲取,同一個線程釋放。

    之前我對,互斥量只由一個線程獲取和釋放,理解的比較狹義,以為這里的獲取和釋放,是系統強制要求的,用?NSLock?實驗發現它可以在不同線程獲取和釋放,感覺很疑惑。

    實際上,的確能在不同線程獲取/釋放同一個互斥鎖,但互斥鎖本來就用于同一個線程中上鎖和解鎖。這里的意義更多在于代碼使用的層面。

    關鍵在于,理解信號量可以允許 N 個信號量允許 N 個線程并發地執行任務。

    @synchonized

    @synchonized 是一個遞歸鎖。

    遞歸鎖

    遞歸鎖也稱為可重入鎖;コ怄i可以分為非遞歸鎖/遞歸鎖兩種,主要區別在于:同一個線程可以重復獲取遞歸鎖,不會死鎖; 同一個線程重復獲取非遞歸鎖,則會產生死鎖。

    因為是遞歸鎖,我們可以寫類似這樣的代碼:

    - (void)testLock{
       if(_count>0){ 
          @synchronized (obj) {
             _count = _count - 1;
             [self testLock];
          }
        }
     }

    而如果換成NSLock,它就會因為遞歸發生死鎖了。

    實際使用問題

    如果obj 為 nil,或者?obj地址不同,鎖會失效。

    所以我們要防止如下的情況:

    @synchronized (obj) {
      obj = newObj;
    }   

    這里的 obj 被更改后,等到其它線程訪問時,就和沒加鎖一樣直接進去了。

    另外一種情況,就是?@synchonized(self). 不少代碼都是直接將self傳入@synchronized當中,而 self 很容易作為一個外部對象,被調用和修改。所以它和上面是一樣的情況,需要避免使用。

    正確的做法是什么?obj 應當傳入一個類內部維護的NSObject對象,而且這個對象是對外不可見的,不被隨便修改的。

    pthread_mutex

    pthread定義了一組跨平臺的線程相關的 API,其中可以使用?pthread_mutex作為互斥鎖。

    pthread_mutex 不是使用忙等,而是同信號量一樣,會阻塞線程并進行等待,調用時進行線程上下文切換。

    pthread_mutex` 本身擁有設置協議的功能,通過設置它的協議,來解決優先級反轉:

    pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol)

    其中協議類型包括以下幾種:

    • PTHREAD_PRIO_NONE:線程的優先級和調度不會受到互斥鎖擁有權的影響。
    • PTHREAD_PRIO_INHERIT:當高優先級的等待低優先級的線程鎖定互斥量時,低優先級的線程以高優先級線程的優先級運行。這種方式將以繼承的形式傳遞。當線程解鎖互斥量時,線程的優先級自動被降到它原來的優先級。該協議就是支持優先級繼承類型的互斥鎖,它不是默認選項,需要在程序中進行設置。
    • PTHREAD_PRIO_PROTECT:當線程擁有一個或多個使用?PTHREAD_PRIO_PROTECT初始化的互斥鎖時,此協議值會影響其他線程(如?thrd2)的優先級和調度。thrd2?以其較高的優先級或者以thrd2擁有的所有互斥鎖的最高優先級上限運行;诒籺hrd2擁有的任一互斥鎖阻塞的較高優先級線程對于?thrd2的調度沒有任何影響。

    設置協議類型為 PTHREAD_PRIO_INHERIT ,運用優先級繼承的方式,可以解決優先級反轉的問題。

    而我們在 iOS 中使用的 NSLock,NSRecursiveLock等都是基于pthread_mutex 做實現的。

    NSLock

    NSLock屬于 pthread_mutex的一層封裝, 設置了屬性為 PTHREAD_MUTEX_ERRORCHECK 。

    它會損失一定性能換來錯誤提示。并簡化直接使用?pthread_mutex 的定義。

    NSCondition

    NSCondition是通過pthread中的條件變量(condition variable)?pthread_cond_t來實現的。

    條件變量

    在線程間的同步中,有這樣一種情況: 線程 A 需要等條件 C 成立,才能繼續往下執行.現在這個條件不成立,線程 A 就阻塞等待. 而線程 B 在執行過程中,使條件 C 成立了,就喚醒線程 A 繼續執行。

    對于上述情況,可以使用條件變量來操作。

    條件變量,類似信號量,提供線程阻塞與信號機制,可以用來阻塞某個線程,等待某個數據就緒后,隨后喚醒線程。

    一個條件變量總是和一個互斥量搭配使用。

    而NSCondition其實就是封裝了一個互斥鎖和條件變量,互斥鎖的lock/unlock方法和后者的wait/signal統一封裝在?NSCondition對象中,暴露給使用者。

    用條件變量控制線程同步,最為經典的例子就是 生產者-消費者問題。

    生產者-消費者問題

    生產者消費者問題,是一個著名的線程同步問題,該問題描述如下:

    有一個生產者在生產產品,這些產品將提供給若干個消費者去消費。要求讓生產者和消費者能并發執行,在兩者之間設置一個具有多個緩沖區的緩沖池,生產者將它生產的產品放入一個緩沖區中,消費者可以從緩沖區中取走產品進行消費,顯然生產者和消費者之間必須保持同步,即不允許消費者到一個空的緩沖區中取產品,也不允許生產者向一個已經放入產品的緩沖區中再次投放產品。

    我們可以剛好可以使用?NSCondition解決生產者-消費者問題。具體的代碼放置在文末的 Demo 里了。

    這里需要注意,實際操作NSCondition做?wait操作時,如果用if判斷:

    if(count==0){
        [condition wait];
    }

    上面這樣是不能保證消費者是線程安全的。

    因為NSCondition可以給每個線程分別加鎖,但加鎖后不影響其他線程進入臨界區。所以?NSCondition使用?wait并加鎖后,并不能真正保證線程的安全。

    當一個signal操作發出時,如果有兩個線程都在做 消費者 操作,那同時都會消耗掉資源,于是繞過了檢查。

    例如我們的條件是,count == 0?執行等待。

    假設當前?count = 0,線程A 要判斷到?count == 0,執行等待;

    線程B 執行了count = 1,并喚醒線程A 執行?count - 1,同時線程C 也判斷到?count > 0?。因為處在不同的線程鎖,同樣判斷執行了?count - 1。2 個線程都會執行count - 1,但是?count = 1,實際就出現count = -1的情況。

    所以為了保證消費者操作的正確,使用 while 循環中的判斷,進行二次確認:

     while (count == 0) {
       [condition wait];
    }
    條件變量和信號量的區別

    每個信號量有一個與之關聯的值,發出時+1,等待時-1,任何線程都可以發出一個信號,即使沒有線程在等待該信號量的值。

    可是對于條件變量,例如?pthread_cond_signal發出信號后,沒有任何線程阻塞在?pthread_cond_wait上,那這個條件變量上的信號會直接丟失掉。

    NSConditionLock

    NSConditionLock稱為條件鎖,只有?condition?參數與初始化時候的?condition相等,lock才能正確進行加鎖操作。

    這里分清兩個概念:

    • unlockWithCondition:,它是先解鎖,再修改 condition 參數的值。 并不是當 condition 符合某個件值去解鎖。
    • lockWhenCondition:,它與?unlockWithCondition:?不一樣,不會修改 condition 參數的值,而是符合 condition 的值再上鎖。

    在這里可以利用?NSConditionLock實現任務之間的依賴.

    NSRecursiveLock

    NSRecursiveLock?和前面提到的?@synchonized一樣,是一個遞歸鎖。

    NSRecursiveLock?與?NSLock?的區別在于內部封裝的pthread_mutex_t?對象的類型不同,NSRecursiveLock?的類型被設置為 PTHREAD_MUTEX_RECURSIVE。

    NSDistributedLock

    這里順帶提一下?NSDistributedLock, 是 macOS 下的一種鎖.

    對于NSDistributedLock 的描述是:

    A lock that multiple applications on multiple hosts can use to restrict access to some shared resource, such as a file

    意思是說,它是一個用在多個主機間的多應用的鎖,可以限制訪問一些共享資源,例如文件。

    按字面意思翻譯,NSDistributedLock?應該就叫做 分布式鎖。

    其它保證線程安全的方式

    除了用鎖之外,有其它方法保證線程安全嗎?

    使用單線程訪問

    首先,盡量避免多線程的設計。因為多線程訪問會出現很多不可控制的情況。有些情況即使上鎖,也無法保證百分之百的安全,例如自旋鎖的問題。

    不對資源做修改

    而如果還是得用多線程,那么避免對資源做修改。

    如果都是訪問共享資源,而不去修改共享資源,也可以保證線程安全。

    比如NSArry作為不可變類是線程安全的。然而它們的可變版本,比如?NSMutableArray?是線程不安全的。事實上,如果是在一個隊列中串行地進行訪問的話,在不同線程中使用它們也是沒有問題的。


    關聯標簽:
    婷婷网久久,丁香五月婷婷五月,久久久久婷婷国产综合,99久久婷婷国产综合精品青