1. 程式人生 > >iOS 線程安全--鎖

iOS 線程安全--鎖

protocol 錯誤 without 報錯 contain 好的 一秒 控制 code

一,前言

  線程安全是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];



iOS 線程安全--鎖