1. 程式人生 > >iOS 執行緒安全--鎖

iOS 執行緒安全--鎖

一,前言

  執行緒安全是iOS開發中避免了的話題,隨著多執行緒的使用,對於資源的競爭以及資料的操作都可能存在風險,所以有必要在操作時保證執行緒安全。

二,為什麼要使用鎖?

     由於一個程序中不可避免的存在多執行緒,所以不可避免的存在多個執行緒訪問同一個資料的情況。但是為了資料的安全性,當一個執行緒訪問資料的時候,其它的執行緒不能對其訪問。簡單來講就是在同一時刻,對同一個資料操作的執行緒只有一個。只有確保了這樣,才能使資料不會被其他執行緒影響。而執行緒不安全,則是在同一時刻可以有多個執行緒對該資料進行訪問,從而得不到預期的結果。例如,一個記憶體單元儲存著一個可讀,可寫的變數資料10,我們想取到10時,另外一個執行緒把它改成11,就會造成我們取到的資料,並不是我們想要的。再比如,寫檔案和讀檔案,當一個執行緒在寫檔案的時候,理論上來說,如果這個時候另一個執行緒來直接讀取的話,那麼得到的結果可能是你無法預料的。

示例:我們定義一個person類,建立一個NSInterge  age的屬性,開闢兩個執行緒去改變age的值。
    

- (void)withoutLock {
    __block Person *p = [[Person alloc]init];
    
    [NSThread detachNewThreadWithBlock:^{ //開闢一個新執行緒
        for (int i = 0; i<1000; i++) {
            p.age ++;
        }
        NSLog(@"%zd\n",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{ //開闢一個新執行緒
        for (int i = 0; i<1000; i++) {
            p.age ++;
        }
        NSLog(@"%zd\n",p.age);
    }];
    
}

 列印結果:

2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339611] 1893
2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339610] 1012

 分析結果:

   按正常的理想情況,列印的結果應該為1000,2000; 造成這個問題的主要原因就是我們開闢的兩個執行緒都去訪問age的記憶體單元,造成資料混亂。

 假如我們加上鎖以後:
 

- (void)useLock {
    __block Person *p = [[Person alloc]init];
    
    NSLock *myLock = [[NSLock alloc]init];
    NSLog(@"begin:");
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i<1000; i++) {
            [myLock lock]; //加鎖
             p.age ++;
            [myLock unlock]; //解鎖
        }
        NSLog(@"%zd\n",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i<1000; i++) {
            [myLock lock]; //加鎖
            p.age ++;
            [myLock unlock]; //解鎖
        }
        NSLog(@"%zd\n",p.age);
    }];
}

 列印結果:

2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339611] 1000
2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339610] 2000

三,怎麼保證執行緒安全

    通常我們使用鎖的機制來保證執行緒安全,即確保同一時刻只有同一個執行緒來對同一個資料來源進行訪問。

四,常用的鎖有哪些

  •  NSLock 
  •  Synchronized 同步鎖
  •  Atomic 自旋鎖
  •  Recursivelock 遞迴鎖
  •  Dispatch_semaphore 訊號量
  •  NSConditionLock和NSCondition 條件鎖

五,常用鎖的使用

  • NSLock

         * 系統API

@protocol NSLocking
lock 方法
- (void)lock //獲得鎖
unlock 方法
- (void)unlock //釋放鎖

 

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}
- (BOOL)tryLock; //試圖得到一個鎖。YES:成功得到鎖;NO:沒有得到鎖。
- (BOOL)lockBeforeDate:(NSDate *)limit; //在指定的時間以前得到鎖。YES:在指定時間之前獲得了鎖;NO:在指定時間之前沒有獲得鎖。該執行緒將被阻塞,直到獲得了鎖,或者指定時間過期。
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); //給鎖定義一個Name
@end     

   * NSLock的執行原理

   某個執行緒A呼叫lock方法。這樣,NSLock將被上鎖。可以執行“關鍵部分”,完成後,呼叫unlock方法。如果,線上程A 呼叫unlock方法之前,另一個執行緒B呼叫了同一鎖物件的lock方法。那麼,執行緒B只有等待。直到執行緒A呼叫了unlock。
       * 使用方法

//初始化資料鎖(主執行緒中)
NSLock *lock =[NSLock alloc]init];
//資料加鎖
[lock lock];

//加鎖的內容
[object doSomeThine];
//資料解鎖 [lock Unlock];

       * 使用示例

//主執行緒中
NSLock *lock = [[NSLock alloc] init];

//執行緒1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  [lock lock];
  NSLog(@"執行緒1");
  sleep(2);
  [lock unlock];
  NSLog(@"執行緒1解鎖成功");
});

//執行緒2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  sleep(1);//以保證讓執行緒2的程式碼後執行
  [lock lock];
  NSLog(@"執行緒2");
  [lock unlock];
});

 結果:

2018-12-02 14:23:09.659 ThreadLockControlDemo[1754:129663] 執行緒1
2018-12-02 14:23:11.663 ThreadLockControlDemo[1754:129663] 執行緒1解鎖成功
2018-12-02 14:23:11.665 ThreadLockControlDemo[1754:129659] 執行緒2

    * 注意事項

Warning
 * NSLock類使用POSIX(可移植性作業系統介面)執行緒來實現上鎖的特性。當NSLock類收到一個解鎖的訊息,你必須確定傳送源也是來自那個傳送上鎖的執行緒。在不同的執行緒上解鎖,會產生不定義行為。

 * 你不應該把這個類實現遞迴鎖。如果在同一個執行緒上呼叫兩次lock方法,將會對這個執行緒永久上鎖。使用NSRecursiveLock類來才可以實現遞迴鎖。
 * 解鎖一個沒有被鎖定的鎖是一個程式錯誤,這個地方需要注意。
  •  Synchronized 同步鎖

   同步鎖是比較常用的,因為其使用方法是所有鎖中最簡單的,但效能卻是最差的,所以對效能要求不高的使用場景Synchronized是一種比較方便的鎖。

         * 使用示例:

static Config * instance = nil;
//方法A
+(Config *) Instance {

    @synchronized(self)  {

        if(nil == instance)  {
            [self new];
        }
    }
    return instance;
}

//方法B
+(id)allocWithZone:(NSZone *)zone {

    @synchronized(self)  {
        if(instance == nil){
            instance = [super allocWithZone:zone];
            return instance;
        }
    }
    return nil;
}

         * 使用介紹:

        @synchronized,代表這個方法加鎖, 相當於不管哪一個執行緒(例如執行緒A),執行到這個方法時,都要檢查有沒有其它執行緒例如B正在用這個方法,有的話要等正在使用synchronized方法的執行緒B執行完這個方法後再執行此執行緒A,沒有的話,直接執行。它包括兩種用法:synchronized 方法和 synchronized 塊。

      @synchronized 方法控制對類(一般在IOS中用在單例中)的訪問:每個類例項對應一把鎖,每個 synchronized 方法都必須獲得呼叫該方法鎖方能執行,否則所屬就會發生執行緒阻塞,方法一旦執行,就獨佔該鎖,直到從該方法返回時才將鎖釋放,此後被阻塞的執行緒方能獲得該鎖,重新進入可執行狀態。這種機制確保了同一時刻對於每一個類,至多隻有一個處於可執行狀態,從而有效避免了類成員變數的訪問衝突(只要所有可能訪問類的方法均被宣告為 synchronized)。

    synchronized 塊:

     @通過 synchronized關鍵字來宣告synchronized 塊。語法如下:

     @synchronized(syncObject) {

     }

      synchronized 塊是這樣一個程式碼塊,其中的程式碼必須獲得物件 syncObject (如前所述,可以是類例項或類)的鎖方能執行,具體機制同前所述。由於可以針對任意程式碼塊,且可任意指定上鎖的物件,故靈活性較高。

         * 使用總結

  1. 從上可以看出不需要建立鎖,一種類似於swift中呼叫一個含有尾隨閉包的函式,就能實現功能。
  2. synchronized內部實現是對傳入的物件,為其分配一個遞迴鎖,儲存在雜湊表中。

         * 使用注意:
            @synchronized(){} 小括號裡面需要傳入一個物件型別,基本資料型別不能作為引數;

            @synchronized(){}小括號內的這個物件不能為空,如果為nil,就不能保證其鎖的功能。

  • Atomic 自旋鎖

         自旋鎖在iOS系統中的實現是OSSpinLock。自旋鎖通過一直處於while盲等狀態,來實現只有一個執行緒訪問資料。由於一直處於while迴圈,所以對CPU的佔用也比較高的,用CPU的消耗換來的好處就是自旋鎖的效能高。

         * 使用介紹

            當上一個執行緒的任務沒有執行完畢的時候(被鎖住),那麼下一個執行緒會一直等待(busy-waiting),當上一個執行緒的任務執行完畢,下一個執行緒會立即執行。

         * 優缺點

      1. 由於自旋鎖不會引起呼叫者睡眠,所以自旋鎖的效率遠高於互斥鎖

                2. 自旋鎖會一直佔用CPU,也可能會造成死鎖

                3.自旋鎖有bug!不同優先順序執行緒排程演算法會有優先順序反轉問題,比如低優先順序獲鎖訪問資源,高優先順序嘗試訪問時會等待,這時低優先順序又沒法爭過高優先順序導致任務無法完成lock釋放不了

         * 原子操作

  • nonatomic:非原子屬性,非執行緒安全,適合小記憶體移動裝置
  • atomic:原子屬性,default,執行緒安全(內部使用自旋鎖),消耗大量資源
    • 單寫多讀,只為setter方法加鎖,不影響getter

    • 相關程式碼如下:

      static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) 
      {
         if (offset == 0) {
             object_setClass(self, newValue);
             return;
         }
      
         id oldValue;
         id *slot = (id*) ((char*)self + offset);
      
         if (copy) {
             newValue = [newValue copyWithZone:nil];
         } else if (mutableCopy) {
             newValue = [newValue mutableCopyWithZone:nil];
         } else {
             if (*slot == newValue) return;
             newValue = objc_retain(newValue);
         }
      
         if (!atomic) {
             oldValue = *slot;
             *slot = newValue;
         } else {
             spinlock_t& slotlock = PropertyLocks[slot];
             slotlock.lock();
             oldValue = *slot;
             *slot = newValue;        
             slotlock.unlock();
         }
      
         objc_release(oldValue);
      }
      
      void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
      {
         bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
         bool mutableCopy = (shouldCopy == MUTABLE_COPY);
         reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
      }
       
    • 總結:很容易理解的程式碼,可變拷貝和不可變拷貝會開闢新的空間,兩者皆不是則持有(引用計數+1),相比nonatomic只是多了一步鎖操作。   

           * 使用示例

#import "ViewController.h"
#import "Person.h"
#import <libkern/OSAtomic.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self  useLock];
}

- (void)withoutLock {
    __block Person *p = [[Person alloc]init];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i<1000; i++) {
            p.age ++;
        }
        NSLog(@"%zd\n",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{
        @synchronized(self){
            
        }
        for (int i = 0; i<1000; i++) {
            p.age ++;
        }
        NSLog(@"%zd\n",p.age);
    }];
    
}

- (void)useLock {
    
    __block OSSpinLock spinLock = OS_SPINLOCK_INIT; //建立鎖
    __block Person *p = [[Person alloc]init];
    
    
    NSLog(@"begin:");
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i<1000; i++) {
            OSSpinLockLock(&spinLock); //加鎖
             p.age ++;
            OSSpinLockLock(&spinLock); //解鎖
        }
        NSLog(@"%zd\n",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i<1000; i++) {
            OSSpinLockLock(&spinLock); //加鎖
            p.age ++;
            OSSpinLockLock(&spinLock); //解鎖
        }
        NSLog(@"%zd\n",p.age);
    }];
}

            * 使用總結

               1)首先需要#important<libkern/OSAtomic.h> ,因此關於自旋鎖的API是在這個檔案中宣告的。

               2)建立自旋鎖也是通過一個靜態巨集,線上程內通過 OSSpinLockLock 和 OSSpinLockUnlock來上鎖,解鎖。如果不是因為現在的OSSpinLock出現了使用bug,在效能以及使用方面來說,都是很好的使用鎖的選擇。

            * 自旋鎖的原理

     就是while迴圈來佔用CPU,實際上,當A執行緒獲取到鎖時,CPU會處於while死迴圈,而這個死迴圈並不是A執行緒造成的,當A獲取到鎖,並且B執行緒也要申請鎖時,就會一直while迴圈詢問A執行緒是否釋放了該鎖,所以導致了CPU死迴圈,因此是B執行緒導致的,這個是“自旋”的由來,正是因為這個一直等待詢問,並不類似於互斥鎖,互斥鎖在申請時處於執行緒休眠狀態,所以才使自旋鎖的效能高。舉個列子:煮飯吃,你的電飯鍋(A執行緒)正在煮飯(資源),而你本人(B執行緒)也想煮飯,你有兩種方式,第一種,一直在電飯鍋前等待著,看著飯好了沒;第二種,去忙其它的,每15分鐘過來看一次飯好了沒。很顯然,按照第一種方式肯定是會先吃上飯。

  • Recursivelock 遞迴鎖

        * 需求場景

     一個鎖只是請求一份資源,而在一些開發實際中,往往需要在程式碼中巢狀鎖的使用,也就是在同一個執行緒中,一個鎖還沒有解鎖就再次加鎖。這個時候就用到了遞迴鎖。

        * 實現原理

    遞迴鎖也是通過 pthread_mutex_lock 函式來實現,在函式內部會判斷鎖的型別。NSRecursiveLock 與 NSLock 的區別在於內部封裝的 pthread_mutex_t 物件的型別不同,前者的型別為 PTHREAD_MUTEX_RECURSIVE

        * 運用場景

    迴圈(多張圖片迴圈上傳),遞迴

        * 使用示例
 示例一:   

//遞迴鎖例項化

    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    static void (^RecursiveMethod)(NSInteger);
    //  同一執行緒可多次加鎖,不會造成死鎖
    RecursiveMethod = ^(NSInteger value){
        [lock lock];//一進來就要開始加鎖
        [NetWorkManager requestWithMethod:POST Url:url Parameters:paraDic success:^(id responseObject) {
            [self reuestForSuccess];
  //一旦資料獲取成功就要解鎖 不然會造成死鎖
              [lock unlock];
        } requestRrror:^(id requestRrror) {
            //條件沒有達到,開始迴圈操作
            if(value > 0){
                RecursiveMethod(value-1);//必須-1  迴圈
            }

            if(value == 0){
            //條件 如果 == 0 代表迴圈的次數條件已經達到 可以做別的操作
            }

            //失敗後也要解鎖
                [lock unlock];
        }];

             //記得解鎖
              [lock unlock];
    };

    //設定遞迴鎖迴圈次數  自定義
       RecursiveMethod(5);

 示例二:

- (void)recursiveLock {
    NSRecursiveLock *theLock = [[NSRecursiveLock alloc]init];
    [self MyRecursiveFucntion:5 recursiveLock:theLock];  
}
- (void) MyRecursiveFucntion:(NSInteger )value recursiveLock:(NSRecursiveLock *)theLock { [theLock lock]; if (value !=0) { --value; [self MyRecursiveFucntion:value recursiveLock:theLock]; } [theLock unlock]; }
  • Dispatch_semaphore 訊號量

    dispatch_semaphore是GCD用來同步的一種方式,與他相關的共有三個函式,分別是

    dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。

下面我們逐一介紹三個函式:

  (1)dispatch_semaphore_create的宣告為:

     dispatch_semaphore_t  dispatch_semaphore_create(long value);

     傳入的引數為long,輸出一個dispatch_semaphore_t型別且值為value的訊號量。

    值得注意的是,這裡的傳入的引數value必須大於或等於0,否則dispatch_semaphore_create會返回NULL。

    (關於訊號量,我就不在這裡累述了,網上很多介紹這個的。我們這裡主要講一下dispatch_semaphore這三個函式的用法)。

  (2)dispatch_semaphore_signal的宣告為:

      long dispatch_semaphore_signal(dispatch_semaphore_t dsema)

      這個函式會使傳入的訊號量dsema的值加1;(至於返回值,待會兒再講)

    (3) dispatch_semaphore_wait的宣告為:

    long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

    這個函式會使傳入的訊號量dsema的值減1;

    這個函式的作用是這樣的,如果dsema訊號量的值大於0,該函式所處執行緒就繼續執行下面的語句,並且將訊號量的值減1;

    如果desema的值為0,那麼這個函式就阻塞當前執行緒等待timeout(注意timeout的型別為dispatch_time_t,

    不能直接傳入整形或float型數),如果等待的期間desema的值被dispatch_semaphore_signal函式加1了,

    且該函式(即dispatch_semaphore_wait)所處執行緒獲得了訊號量,那麼就繼續向下執行並將訊號量減1。

    如果等待期間沒有獲取到訊號量或者訊號量的值一直為0,那麼等到timeout時,其所處執行緒自動執行其後語句。

  (4)dispatch_semaphore_signal的返回值為long型別,當返回值為0時表示當前並沒有執行緒等待其處理的訊號量,其處理

     的訊號量的值加1即可。當返回值不為0時,表示其當前有(一個或多個)執行緒等待其處理的訊號量,並且該函式喚醒了一

    個等待的執行緒(當執行緒有優先順序時,喚醒優先順序最高的執行緒;否則隨機喚醒)。

    dispatch_semaphore_wait的返回值也為long型。當其返回0時表示在timeout之前,該函式所處的執行緒被成功喚醒。

    當其返回不為0時,表示timeout發生。

  (5)在設定timeout時,比較有用的兩個巨集:DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER。

     DISPATCH_TIME_NOW  表示當前;

     DISPATCH_TIME_FOREVER  表示遙遠的未來;

    一般可以直接設定timeout為這兩個巨集其中的一個,或者自己建立一個dispatch_time_t型別的變數。

    建立dispatch_time_t型別的變數有兩種方法,dispatch_time和dispatch_walltime。

    利用建立dispatch_time建立dispatch_time_t型別變數的時候一般也會用到這兩個變數。

    dispatch_time的宣告如下:

      dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);

      其引數when需傳入一個dispatch_time_t型別的變數,和一個delta值。表示when加delta時間就是timeout的時間。

    例如:dispatch_time_t  t = dispatch_time(DISPATCH_TIME_NOW, 1*1000*1000*1000);

       表示當前時間向後延時一秒為timeout的時間。

    (6)關於訊號量,一般可以用停車來比喻。

    停車場剩餘4個車位,那麼即使同時來了四輛車也能停的下。如果此時來了五輛車,那麼就有一輛需要等待。

    訊號量的值就相當於剩餘車位的數目,dispatch_semaphore_wait函式就相當於來了一輛車,dispatch_semaphore_signal

    就相當於走了一輛車。停車位的剩餘數目在初始化的時候就已經指明瞭(dispatch_semaphore_create(long value)),

    呼叫一次dispatch_semaphore_signal,剩餘的車位就增加一個;呼叫一次dispatch_semaphore_wait剩餘車位就減少一個;

    當剩餘車位為0時,再來車(即呼叫dispatch_semaphore_wait)就只能等待。有可能同時有幾輛車等待一個停車位。有些車主

    沒有耐心,給自己設定了一段等待時間,這段時間內等不到停車位就走了,如果等到了就開進去停車。而有些車主就像把車停在這,

    所以就一直等下去。

  (7)程式碼舉簡單示例如下:

     dispatch_semaphore_t signal;      signal = dispatch_semaphore_create(1);      __block  long  x = 0;      NSLog (@ "0_x:%ld" ,x);      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{          sleep(1);          NSLog (@ "waiting" );          x = dispatch_semaphore_signal(signal);          NSLog (@ "1_x:%ld" ,x);            sleep(2);          NSLog (@ "waking" );          x = dispatch_semaphore_signal(signal);          NSLog (@ "2_x:%ld" ,x);      }); //    dispatch_time_t duration = dispatch_time(DISPATCH_TIME_NOW, 1*1000*1000*1000); //超時1秒 //    dispatch_semaphore_wait(signal, duration);        x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);      NSLog (@ "3_x:%ld" ,x);        x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);      NSLog (@ "wait 2" );      NSLog (@ "4_x:%ld" ,x);        x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);      NSLog (@ "wait 3" );      NSLog (@ "5_x:%ld" ,x);

最終列印的結果為:

  2018-12-02 22:51:54.734 LHTest[15700:70b] 0_x:0 2018-12-02 22:51:54.737 LHTest[15700:70b] 3_x:0 2018-12-02 22:51:55.738 LHTest[15700:f03] waiting 2018-12-02 22:51:55.739 LHTest[15700:70b] wait 2 2018-12-02 22:51:55.739 LHTest[15700:f03] 1_x:1 2018-12-02 22:51:55.739 LHTest[15700:70b] 4_x:0 2018-12-02 22:51:57.741 LHTest[15700:f03] waking 2018-12-02 22:51:57.742 LHTest[15700:f03] 2_x:1 2018-12-02 22:51:57.742 LHTest[15700:70b] wait 3 2018-12-02 22:51:57.742 LHTest[15700:70b] 5_x:0
  •  NSConditionLock和NSCondition 條件鎖

    * 使用介紹
    NSConditionLock好處是可以設定條件,條件符合時獲得鎖。設定時間,指定時間之前獲取鎖。缺點是加鎖和解鎖需要在同一執行緒中執行,否則控制檯會報錯,雖然不影響程式執行。(but好像會影響程序釋放,因為多次執行後進程到了80多,程式卡了還是崩潰了,忘了。只是猜測。)

      * 使用舉例

 NSConditionLock * conditionLock = [[NSConditionLockalloc] init];
      //當條件符合時獲得鎖
      [conditionLock lockWhenCondition:1];
     //在指定時間前嘗試獲取鎖,若成功則返回YES 否則返回NO
      BOOL isLock = [conditionLock lockBeforeDate:date1];
    //在指定時間前嘗試獲取鎖,且條件必須符合
      BOOL isLock = [conditionLock lockWhenCondition:1 beforeDate:date1];
    //解鎖並設定條件為2
     [conditionLock unlockWithCondition:2];