iOS中的幾種鎖的總結,三種開啟多執行緒的方式(GCD、NSOperation、NSThread)

學習內容

歡迎關注我的iOS學習總結——每天學一點iOS://github.com/practiceqian/one-day-one-iOS-summary

OC中的幾種鎖

為什麼要引入鎖:在多執行緒編程中,並發會使多個執行緒在同一時間內爭搶同一資源,因此可能造成資源的數據不一致性,為了解決這個問題就引入了鎖

自旋鎖:自旋鎖在訪問被加鎖資源時,調用者執行緒不會進行休眠,而是不停循環在那裡,等待被鎖資源被釋放(不斷循環的狀態也叫做忙等狀態)
互斥鎖:互斥鎖在訪問被加鎖資源時,調用者執行緒會進如休眠狀態,此時cpu會調用其他的執行緒工作,直到被加鎖資源釋放,喚醒休眠執行緒。
優缺點:自旋鎖由於不會引起調用者執行緒休眠,所以不會進行cpu調度以及時間片輪轉等耗時操作,所以如果能在很短的時間內獲得鎖,自旋鎖的效率遠高於互斥鎖,缺點是自旋鎖會一直佔用cpu,在未獲得鎖的情況下一直運行,如果不能短時間內獲得鎖,會使cpu的運行效率降低,同時自旋鎖不能實現遞歸調用
  1. OSSpinLock進行加鎖

    • //OSSpinLock自旋鎖的初始化
      OSSpinLock _lock = OS_SPINLOCK_INIT;
      //鎖定
      OSSpinLockLock(&_lock);
      //解鎖
      OSSpinLockUnlock(&_lock);
      
    • OSSpinLock現在不能繼續保證執行緒安全,因此不建議使用

  2. @synchronized實現加鎖

    • //假設共有十張票,有多個執行緒同時進行買票操作,使用synchronized控制每次只能允許一個執行緒進行訪問
      -(void)saleTickets{
          @synchronized (self) {
              if (self.tickets>0) {
                  self.tickets -= 1;
                  NSLog(@"tickets:%ld---%@",(long)self.tickets,[NSThread currentThread]);
              }else
                  return;
          }
      }
      - (IBAction)clickToPushB:(UIButton *)sender {
          while (self.tickets>0) {
           dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
              [self saleTickets];
            });
          }
      }
      
  3. dispatch_semaphore訊號量實現加鎖

    • 每當發送一個訊號時,訊號量加一

    • 每當發送一個等待訊號時,訊號量減一

    • 如果訊號量為0,則訊號處於等待狀態,直到訊號量大於0才開始執行

    • while (self.tickets>0) {
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        [self saleTickets];
        dispatch_semaphore_signal(sema);
        });
      }
      
    • 訊號量也能用來控制執行緒的最大並發數

  4. 使用pthread來進行加鎖

    • static pthread_mutex_t pLock;
      pthread_mutex_init(&pLock, NULL);
      while (self.tickets>0) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
          pthread_mutex_lock(&pLock);
          [self saleTickets];
          pthread_mutex_unlock(&pLock);
        });
      }
      
    • 加鎖後只能有一個執行緒可以進行某個操作,後面的執行緒再進行時需要排隊,並且lock和unlock是對應出現的,同一個執行緒不能進行多次lock(遞歸鎖允許其在未釋放自己擁有的鎖時,反覆對該資源進行加鎖)

  5. 使用NSLock來進行加鎖

    • self.lock = [NSLock new];
      while (self.tickets>0) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
          [self.lock lock];
          [self saleTickets];
          [self.lock unlock];
        });
      }
      
  6. 使用NSConditionLock條件鎖進行加鎖

    • self.conditionLock = [NSConditionLock new];
      while (self.tickets>0) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
          [self.conditionLock lock];
          [self saleTickets];
          [self.conditionLock unlock];
        });
      }
      
    • //NSConditonLock條件鎖同時也能用來控制執行緒同步
      self.conditionLock = [NSConditionLock new];
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self.conditionLock lock];
        NSLog(@"1");
        [self.conditionLock unlockWithCondition:2];
      
      });
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self.conditionLock lockWhenCondition:2];
        NSLog(@"2");
        [self.conditionLock unlock];
      });
      
  7. 使用NSRecursiveLock(遞歸鎖)進行加鎖

    • 遞歸鎖主要用在循環或者遞歸調用中

    • //下面的程式碼中RecrsiveBlock是遞歸調用的,如果使用NSLock的話,每次進入RecursiveLock的程式碼中都會進行一次加鎖,但是因為沒有解鎖,所以需要等待解鎖,這樣執行緒就會進入阻塞狀態,但是這裡使用了NSRecursiveLock就不會進入阻塞狀態
      NSRecursiveLock *rLock = [NSRecursiveLock new];
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^RecursiveBlock)(int);
        RecursiveBlock = ^(int value) {
          [rLock lock];
          if (value > 0) {
            NSLog(@"執行緒%d", value);
            RecursiveBlock(value - 1);
          }
          [rLock unlock];
        };
        RecursiveBlock(4);
      });
      
  8. 幾種鎖的性能對比

    • 鎖的性能從高到低依次如下
    • OSSpinLock
    • Dispatch_semaphore
    • Pthread_mutex
    • NSLock
    • NSCondition
    • Pthread_mutex(recursive)
    • NSRecursiveLock
    • NSConditionLock
    • @synchronized

iOS中多執行緒總結

  1. GCD(Grand Central Dispatch)

    GCD的兩個核心概念
    • 隊列:這裡的隊列指的是執行任務的等待隊列,即用來存放任務的隊列,隊列是一種特殊的線性表,採用FIFO(先進先出的)的原則,新任務總是被插入到隊列的末尾,而讀取任務的時總是從隊列的頭部開始讀取,每讀取一個任務,則從隊列中釋放一個任務
      • 串列隊列(serial dispatch queue)
        • 每次只有一個任務被執行,讓任務一個接著一個執行(只開啟一個執行緒,一個執行緒執行完畢後再執行下一個任務)
      • 並發隊列
        • 可以讓多個任務同時(並發)執行(可以開啟多個執行緒,並發執行任務)
    • 任務:任務就是我們需要執行的操作,即放入隊列中的那段程式碼,在GCD中任務是放在block中執行的,執行任務有兩種方式,同步執行(sync),非同步執行(async),兩者的主要區別是,是否等待隊列的任務執行結束,以及是否具備開啟新執行緒的能力
      • 同步任務(sync)
        • 同步添加任務到執行的隊列中,在添加的任務執行結束之前,會一直等待,知道隊列裡面的任務完成之後再繼續執行
        • 只能在當前執行緒中執行任務,不具備開啟新執行緒的能力
      • 非同步任務(async)
        • 非同步添加任務到指定的隊列中,它不會做任何等待,可以繼續執行任務
        • 可以在新的執行緒中執行任務,具備開啟新執行緒的能力(但是並不一定會開啟新執行緒,這與指定的隊列有關)
    • 任務和隊列不同組合方式的區別

    • 並發隊列 串列隊列 主隊列
      同步任務(sync) 不開啟新執行緒,串列執行 不開啟新執行緒,串列執行 不開啟新執行緒,造成死鎖
      非同步任務(async) 開啟新執行緒,並發執行 開啟一條新執行緒,串列執行 不開啟新執行緒,串列執行任務
    • 隊列嵌套的情況下,不同組合方式的區別

      • 在非同步串列隊列中增加同一個隊列的串列同步任務同樣也會造成死鎖,原理和主隊列同步任務是相同的

      • dispatch_queue_t serial_queue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
        dispatch_async(serial_queue, ^{
        NSLog(@"serial_queue:%@",[NSThread currentThread]);
        dispatch_sync(serial_queue, ^{
        NSLog(@"serial_queue_sync:%@",[NSThread currentThread]);
        });
        });
        
      • 主隊列同步任務造成死鎖的原因:在主隊列追加同步任務,當執行到同步任務時,主隊列中剩下的任務需要等待追加任務的完成才能繼續往下執行,但是追加的任務需要等待主隊列中的任務按順序往下執行完才能繼續執行。(主隊列中追加的任務和主隊列中本身的任務相互等待,最終造成死鎖)

    • 在iOS中UI的更新必須在主執行緒中執行

      • dispatch_async(dispatch_get_main_queue(), ^{
          [self.view addSubview:self.scrollView];
        });
        
  2. 使用NSOperation實現多執行緒

    NSOperation是個抽象類,並不具備封裝操作的能力,所以我們需要使用它們的子類
    • NSBlockOperation

      • NSLog(@"1---%@",[NSThread currentThread]);
        NSBlockOperation* opeartion = [NSBlockOperation new];
        [opeartion addExecutionBlock:^{
          NSLog(@"2---%@",[NSThread currentThread]);
        
        }];
        [opeartion addExecutionBlock:^{
          NSLog(@"4---%@",[NSThread currentThread]);
        }];
        NSLog(@"3---%@",[NSThread currentThread]);
        [opeartion start];
        -----------------------------------------------------
        1---<NSThread: 0x6000014d0a00>{number = 1, name = main}
        3---<NSThread: 0x6000014d0a00>{number = 1, name = main}
        2---<NSThread: 0x6000014d0a00>{number = 1, name = main}
        4---<NSThread: 0x6000014a1140>{number = 6, name = (null)}
        
      • 當NSBlockOperation中只有一個任務的話是同步執行,當使用addExecutionBlock:添加了多個任務的話,那麼就會默認開啟新的執行緒

    • NSInvocationBlock

      • //NSInvocationOperation默認是在當前執行緒進行同步執行的,除非自定義額外執行緒執行Operation
        NSInvocationOperation* invokeOp = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(saleTickets) object:nil];
        [invokeOp start];
        -------------------------------------------------------------------
        //在非同步並發隊列中增加NSOperation的操作,此時就是非同步執行
        NSInvocationOperation* invokeOp = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(saleTickets) object:nil];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
          [invokeOp start];
        });
        
    • NSOperationQueue

      • 將NSoperation操作(NSBlockOperation和NSInvocationOperation)添加到NSOperationQueue中默認是開啟了多執行緒操作,除了添加這兩種封裝的操作對象外,NSOperationQueue自己也可以添加操作

        • //操作隊列自身添加操作,同樣也是非同步執行的
          NSOperationQueue* queue = [NSOperationQueue new];
          [queue addOperationWithBlock:^{
          	NSLog(@"operationQueue");
          }];
          
      • NSOperationQueue中的操作可以添加依賴,指定任務執行的順序,同時也可以指定開啟的最大執行緒數量

        • queue.maxConcurrentOperationCount = 1;
          [op1 addDependency:op2];
          [queue addOperation:op2];
          [queue addOperation:op1];
          
    • 可以自定義子類繼承自NSoperation,但是需要實現內部相應的方法

  3. 使用NSThread實現多執行緒

    • //創建方式一,能手動拿到執行緒對象,但是需要手動啟動執行緒(start)
      NSThread* thread = [[NSThread alloc]initWithBlock:^{
        NSLog(@"threadTest---%@",[NSThread currentThread]);
      }];
      [thread start];
      //創建方式二,自啟動執行緒,但是無法拿到執行緒對象
      [NSThread detachNewThreadWithBlock:^{
        NSLog(@"threadTest---%@",[NSThread currentThread]);
      }];
      //創建方式三,自啟動執行緒,不能拿到執行緒對象
      [self performSelectorInBackground:@selector(saleTickets) withObject:nil];
      //方式四,自定義執行緒繼承自NSThread,需要重寫main方法
      ....
       
      //指定操作,指定執行緒,waitUntilDone(是否等指定的操作完成之後再進行後面的任務)
      [self performSelector:@selector(saleTickets) onThread:[NSThread mainThread] withObject:nil waitUntilDone:false];
      
Tags: