鎖的原理

  • 鎖的種類

    • 互斥鎖(Mutual exclusion,縮寫 Mutex)
      防止兩條線程同時對同一公共資源(好比全局變量)進行讀寫的機制。當獲取鎖操做失敗時,線程會進入睡眠,等待鎖釋放時被喚醒。 互斥鎖又分爲遞歸鎖和非遞歸鎖。
      • 遞歸鎖
        可重入鎖,同一個線程在鎖釋放前可再次獲取鎖,便可以遞歸調用。
        例如:@synchronized
      • 非遞歸鎖
        不可重入,必須等鎖釋放後才能再次獲取鎖。
        例如:NSLockpthread_mutex
    • 自旋鎖
      線程反覆檢查鎖變量是否可⽤。因爲線程在這⼀過程當中保持執⾏, 所以是⼀種忙等待。⼀旦獲取了⾃旋鎖,線程會⼀直保持該鎖,直⾄顯式釋放⾃旋鎖。⾃旋鎖避免了進程上下⽂的調度開銷,所以對於線程只會阻塞很 短期的場合是有效的。與互斥鎖最大的區別就在於互斥鎖會進入休眠狀態等待被喚醒,而自旋鎖則不會休眠處於忙等待狀態
    • 條件鎖
      就是條件變量,當進程的某些資源要求不滿⾜時就進⼊休眠,也就 是鎖住了。當資源被分配到了,條件鎖打開,進程繼續運⾏
      • NSCondition
      • NSConditionLock
    • 遞歸鎖
      就是同⼀個線程能夠加鎖N次⽽不會引起死鎖
      • NSRecursiveLock
      • pthread_mutex(recursive)
    • 信號量(semaphore)
      是⼀種更⾼級的同步機制,互斥鎖能夠說是semaphore在僅取值0/1時的特例。信號量能夠有更多的取值空間,⽤來實 現更加複雜的同步,⽽不僅僅是線程間互斥。
      • dispatch_semaphore
    • 讀寫鎖
      讀寫鎖實際是⼀種特殊的⾃旋鎖,它把對共享資源的訪問者劃分紅讀者和寫者,讀者只對共享資源 進⾏讀訪問,寫者則須要對共享資源進⾏寫操做。這種鎖相對於⾃旋鎖⽽⾔,能提⾼併發性,由於 在多處理器系統中,它容許同時有多個讀者來訪問共享資源,最⼤可能的讀者數爲實際的邏輯CPU 數。寫者是排他性的,⼀個讀寫鎖同時只能有⼀個寫者或多個讀者(與CPU數相關),但不能同時 既有讀者⼜有寫者。在讀寫鎖保持期間也是搶佔失效的。 若是讀寫鎖當前沒有讀者,也沒有寫者,那麼寫者能夠⽴刻得到讀寫鎖,不然它必須⾃旋在那⾥, 直到沒有任何寫者或讀者。若是讀寫鎖沒有寫者,那麼讀者能夠⽴即得到該讀寫鎖,不然讀者必須 ⾃旋在那⾥,直到寫者釋放該讀寫鎖。
      ⼀次只有⼀個線程能夠佔有寫模式的讀寫鎖, 可是能夠有多個線程同時佔有讀模式的讀寫鎖. 正 是由於這個特性, 當讀寫鎖是寫加鎖狀態時, 在這個鎖被解鎖以前, 全部試圖對這個鎖加鎖的線程都會被阻塞. 當讀寫鎖在讀加鎖狀態時, 全部試圖以讀模式對它進⾏加鎖的線程均可以獲得訪問權, 可是若是 線程但願以寫模式對此鎖進⾏加鎖, 它必須直到全部的線程釋放鎖. 一般, 當讀寫鎖處於讀模式鎖住狀態時, 若是有另外線程試圖以寫模式加鎖, 讀寫鎖一般會阻塞 隨後的讀模式鎖請求, 這樣能夠避免讀模式鎖⻓期佔⽤, ⽽等待的寫模式鎖請求⻓期阻塞. 讀寫鎖適合於對數據結構的讀次數⽐寫次數多得多的狀況. 由於, 讀模式鎖定時能夠共享, 以寫 模式鎖住時意味着獨佔, 因此讀寫鎖⼜叫共享-獨佔鎖.
    • 總結
      基本的鎖其實就只有三類:自旋鎖、互斥鎖、讀寫鎖。其餘的好比條件鎖、遞歸鎖、信號量都是上層的封裝實現
  • 鎖的用法及源碼探索

    在探索使用及源碼以前先看一張圖,上面展現的全部鎖的性能對比aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yMzY3NTQ0NS1lYzhkMWY0ODJiZjFjMmMzLnBuZw.png
    • 準備工做

      在探索源碼以前先寫一個票的demo,先看沒有加鎖的狀況下運行是怎樣的swift

      - (void)viewDidLoad {
            [super viewDidLoad];
            // Do any additional setup after loading the view.
            self.ticketCount = 20;
            [self lg_testSaleTicket];
        }
      
      
        - (void)lg_testSaleTicket{
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                for (int i = 0; i < 5; i++) {
                    [self saleTicket];
                }
            });
      
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                for (int i = 0; i < 5; i++) {
                    [self saleTicket];
                }
            });
      
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                for (int i = 0; i < 3; i++) {
                    [self saleTicket];
                }
            });
      
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                for (int i = 0; i < 10; i++) {
                    [self saleTicket];
                }
            });
        }
      
        - (void)saleTicket{
            if (self.ticketCount > 0) {
                self.ticketCount--;
                sleep(0.1);
                NSLog(@"當前餘票還剩:%ld張",self.ticketCount);
      
            }else{
                NSLog(@"當前車票已售罄");
            }
      
        }
      複製代碼

      運行結果: image.png緩存

    • @synchronized

      從上面的實例代碼中能夠看到多線程訪問同一個數據的時候會出現問題,可能同時一個多個線程訪問一個數據,此時爲了不這種問題能夠加鎖同時只讓一個線程訪問數據,具體用法以下:image.png再看運行結果: image.png發現測試就沒有上述問題了。
      再看源碼實現 首先開啓彙編調試 image.pngimage.png 發現底層會調用兩個方法:objc_sync_exitobjc_sync_enter,也能夠經過clang查看編譯後的文件驗證image.png此時在下個符號斷點objc_sync_exitobjc_sync_enterimage.png發現源碼在libobjc.A.dylib庫中,而後再去庫裏面找源碼image.pngimage.pngimage.png
      先看SyncData結構安全

      typedef struct alignas(CacheLineSize) SyncData {
            struct SyncData* nextData;
            DisguisedPtr<objc_object> object;
            int32_t threadCount;  // number of THREADS using this block
            recursive_mutex_t mutex;
        } SyncData;
      複製代碼

      發現是一個單鏈表結構markdown

      • nextData指向下一個SyncData
      • object一個對象指針,對象是objc_object即OC對象,不難猜想,它保存了被鎖定對象obj的指針
      • threadCount記錄正在使用這個代碼塊的線程數
      • mutex遞歸鎖、獲取到該結構體對象後,就是調用它的lock()方法

      再看id2data源碼 image.png具體源碼註釋圖中都有解釋。
      總結一下大體流程:數據結構

      1. 從線程緩存中查找若是能查找到說明當前線程有被使用鎖因此此時只須要lockCount+1返回就好,objc_sync_exit方法對應的是減一
      2. 若是線程緩存中找不到則衝緩存中查找若是能找到一樣的只須要lockCount+1返回,objc_sync_exit方法對應的是減一
      3. 若是在緩存中沒有找到則說明當前線程一次鎖都還沒添加過,此時則去遍歷緩存查看是否有其餘線程使用過,若是有threadCount加1而且存到緩存中,若是也沒有其餘線程使用則threadCount置爲1存到緩存中

      緩存結構圖: 未命名文件(32).jpg多線程

    • OSSpinLock

      OSSpinLock被棄用,其替代方案是內部封裝了os_unfair_lock,而os_unfair_lock在加鎖時會處於休眠狀態,而不是自旋鎖的忙等狀態併發

    • atomic(原子鎖)

      atomic適用於OC中屬性的修飾符,其自帶一把自旋鎖,屬性在調用settergetter方法的時候會加一把鎖框架

      static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
        {
           ...
           id *slot = (id*) ((char*)self + offset);
           ...
      
            if (!atomic) {//未加鎖
                oldValue = *slot;
                *slot = newValue;
            } else {//加鎖
                spinlock_t& slotlock = PropertyLocks[slot];
                slotlock.lock();
                oldValue = *slot;
                *slot = newValue;        
                slotlock.unlock();
            }
            ...
        }
      複製代碼
      id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
            if (offset == 0) {
                return object_getClass(self);
            }
      
            // Retain release world
            id *slot = (id*) ((char*)self + offset);
            if (!atomic) return *slot;
      
            // Atomic retain release world
            spinlock_t& slotlock = PropertyLocks[slot];
            slotlock.lock();//加鎖
            id value = objc_retain(*slot);
            slotlock.unlock();//解鎖
      
            // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
            return objc_autoreleaseReturnValue(value);
        }
      複製代碼

      從源碼中能夠看出,對於atomic修飾的屬性,進行了spinlock_t加鎖處理,可是在前文中提到OSSpinLock已經廢棄了,這裏的spinlock_t在底層是經過os_unfair_lock替代了OSSpinLock實現的加鎖async

    • pthread_mutex

      pthread_mutex就是互斥鎖,當鎖被佔用,其餘線程申請鎖時,不會一直忙等待,而是阻塞線程並睡眠
      使用示例函數

      // 導入頭文件
       #import <pthread.h>
      
       // 全局聲明互斥鎖
       pthread_mutex_t _lock;
      
       // 初始化互斥鎖
       pthread_mutex_init(&_lock, NULL);
      
       // 加鎖
       pthread_mutex_lock(&_lock);
       // 這裏作須要線程安全操做
       // 解鎖 
       pthread_mutex_unlock(&_lock);
      
       // 釋放鎖
       pthread_mutex_destroy(&_lock);
      複製代碼
    • NSLock

      首先通關斷點調試查看NSLock源碼的位置以下圖: image.png 此時發現NSLock的源碼在Foundation框架中,由於OCFoundation框架是閉源的因此看不了源碼,可是swiftFoundation框架是開源的,因此咱們也已查看swiftFoundation框架,由於也就是語法不同大致實現邏輯都差很少 image.png 能夠發現NSLock底層就是對pthread_mutex的封裝,應爲NSLock是一把互斥鎖,會阻塞線程等待任務執行,因此使用NSLock須要注意不能重入NSLock鎖,會形成線程相互等待的狀況,形成死鎖

    • NSRecursiveLock

      是互斥鎖中的遞歸鎖,可被同一線程屢次獲取的鎖,而不會產生死鎖。什麼意思呢,一個線程已經得到了鎖,開始執行受鎖保護的代碼(鎖還未釋放),若是這段代碼調用了其餘函數,而被調用的函數又要獲取這個鎖,此時已然能夠得到鎖並正常執行,而不會死鎖。底層也是對pthread_mutex的封裝底層實現代碼也和NSLock很想lock方法和unLock方法都和NSLock是同樣的無非就是init的時候NSRecursiveLock設置了該鎖的類型是個遞歸鎖iShot2021-04-20 17.03.59.png 使用示例: image.png

    • NSCondition

      NSCondition也是一把互斥鎖他和NSLock的區別在於
      NSLock在獲取不到鎖的時候自動使線程進入休眠,鎖被釋放後線程又自動被喚醒
      NSCondition可使咱們更加靈活的控制線程狀態,在任何須要的時候使線程進入休眠或喚醒它

      • 主要API

      image.png

      • 使用場景及示例

      例如一個生產消費的例子,只有生產出來了商品才能被消費者售賣,消費者再買東西的時候商品沒了就要等待生產者產出後在進行購買,示例代碼以下:

      - (void)td_testConditon{
            _testCondition = [[NSCondition alloc] init];
            //建立生產-消費者
            for (int i = 0; i < 50; i++) {
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                    [self td_producer];
                });
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                    [self td_consumer];
                });
      
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                    [self td_consumer];
                });
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                    [self td_producer];
                });
            }
        }
      
        - (void)td_producer{
            [_testCondition lock]; // 操做的多線程影響
            self.ticketCount = self.ticketCount + 1;
            NSLog(@"生產一個 現有 count %zd",self.ticketCount);
            [_testCondition signal]; // 信號
            [_testCondition unlock];
        }
      
        - (void)td_consumer{
      
             [_testCondition lock];  // 操做的多線程影響
            if (self.ticketCount == 0) {
                NSLog(@"等待 count %zd",self.ticketCount);
                [_testCondition wait];
            }
            //注意消費行爲,要在等待條件判斷以後
            self.ticketCount -= 1;
            NSLog(@"消費一個 還剩 count %zd ",self.ticketCount);
             [_testCondition unlock];
        }
      複製代碼
      • 源碼探索

      iShot2021-04-21 09.19.22.png 底層和NSLock很像都是對pthread_mutex_t的封裝,無非就是使用了pthread_cond_t的條件

    • NSConditionLock

      條件鎖,通俗的將就是有條件的互斥鎖

      使用NSConditionLock對象,能夠確保線程僅在知足特定條件時才能獲取鎖。 一旦得到了鎖並執行了代碼的關鍵部分,線程就能夠放棄該鎖並將關聯條件設置爲新的條件。 條件自己是任意的:您能夠根據應用程序的須要定義它們。

      • 使用示例
        #pragma mark -- NSConditionLock
          - (void)td_testConditonLock{
              // 信號量
              NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
        
              dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                   [conditionLock lockWhenCondition:1]; // conditoion = 1 內部 Condition 匹配
                  // -[NSConditionLock lockWhenCondition: beforeDate:]
                  NSLog(@"線程 1");
                   [conditionLock unlockWithCondition:0];
              });
        
              dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        
                  [conditionLock lockWhenCondition:2];
                  sleep(0.1);
                  NSLog(@"線程 2");
                  // self.myLock.value = 1;
                  [conditionLock unlockWithCondition:1]; // _value = 2 -> 1
              });
        
              dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
                 [conditionLock lock];
                 NSLog(@"線程 3");
                 [conditionLock unlock];
              });
          }
        複製代碼
        image.png
      • 示例代碼分析 image.png
      • 源碼分析 image.png 從源碼不難看出NSConditionLock的源碼其實就是NSConditionNSLock結合封裝的一把鎖
相關文章
相關標籤/搜索