iOS-多線程知識點整理

多線程.png

iOS中多線程

首先看一道面試題

iOS中多線程有哪些實現方案?

技術方案 簡介 語言 線程生命周期 使用頻率
pthread 1. 一套通用的多線程API2. 跨平臺/可移植3. 使用難度大 C 程序員管理 幾乎不用
NSThread 1.面向對象 2.簡單易用直接操作線程對象 OC 程序員管理 偶爾使用
GCD 1.旨在替代NSThread等線程技術 2.充分利用設備的多核 C 自動管理 經常使用
NSOperation 1.基于GCD 2.比GCD多了一些更實用的函數 OC 自動管理 經常使用

iOS中,多線程一般有三種方案GCDNSOperationNSThread

一、GCD

GCD相關問題一般分為三個方面:首先,同步/異步串行/并發問題;其次,dispatch_barrier_async異步柵欄調用,解決多讀單寫問題;最后,dispatch_group使用和理解。

GCD中有2種用來執行任務的方式:同步和異步;同時還有兩種類型的隊列:并發和串行隊列。并發隊列讓多個任務并發執行,自動開啟多個線程同時執行任務。并發功能只在異步函數下才生效。

并發隊列 手動創建的串行隊列 主隊列
同步 沒有開辟新線程 串行執行 沒有開辟新線程 串行執行 沒有開辟新線程 串行執行
異步 有開辟新線程 并發執行 有開辟新線程 串行執行 沒有開辟新線程 串行執行

注意 :使用同步函數往當前串行隊列中添加任務,會卡主當前的串行隊列,產生死鎖。

1.1 同步/異步串行/并發

存在四種組合方案:

// 同步 + 串行
dispatch_sync(serial_queue, ^{
//任務
});
// 異步 + 串行
dispatch_async(serial_queue, ^{
//任務
});
// 同步 + 并發
dispatch_sync(concurrent_queue, ^{
//任務
});
// 異步 + 并發
dispatch_async(concurrent_queue, ^{
//任務
});

1.1.1 同步 + 串行

- (void)viewDidLoad {
dispatch_sync(dispatch_get_main_queue(), ^{
      [self doSomething];
});
}

上面這段代碼,存在什么問題?


904629-d3bc4b362fbf2673.png

產生死鎖。隊列引起循環等待。因為,viewDidLoad()進入主隊列,執行過程中會將block添加到主隊列中。viewDidLoad()需要等待block執行完成后才能結束,由于主隊列先進先出的block需要viewDidLoad()執行完畢才能執行。因此導致隊列循環等待的問題。上圖中,首先向主隊列中提交了一個 viewDidLoad 的任務,后續又提交了一個 Block 任務 。 現在分派 viewDidLoad
到主線程中去執行,在它執行過程中需要調用 Block ,等 Block 同步調用完成之后,這個 viewDidLoad 才能繼續向下走,所以viewDidLoad 的調用結束依賴于 Block 方法執行完。 而隊列是遵循先進先出(FIFO)原則的,Block 要想執行,就必須等待 viewDidLoad 調用完成。 由此就產生了隊列的循環等待,造成死鎖.

上面的問題理解,在來看一個問題。

- (void)viewDidLoad {
dispatch_sync(serial_queue, ^{
      [self doSomething];
});
}

上面的代碼,有什么問題?


904629-2daebf30855fdcaa.png

沒有問題。這里是將block添加到單獨的串行隊列。viewDidLoad()在主隊列中在主線程中執行,在其執行過程中調用block添加到串行隊列中,在主線程中執行。viewDidLoad 在主隊列中,提交到主線程處理,在 viewDidLoad方法運行到某一時刻的時候,會提交一個任務到串行隊列上。串行隊列同步提交一個 Block任務,因為是同步的(同步提交就是在當前線程執行),所以串行隊列中的任務也是提交到主線程中執行,當串行隊列這個任務在主線程處理完成之后,再繼續處理viewDidLoad 后續的代碼邏輯.。同步方式提交任務,無論在串行隊列還是并發隊列都會在當前線程中執行。

1.1.2 同步 + 并發

int main(int argc, const char * argv[]) {
  @autoreleasepool {
      dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
      NSLog(@"1");
      dispatch_sync(global_queue, ^{
          NSLog(@"2");
          dispatch_sync(global_queue, ^{
              NSLog(@"3");
          });
          NSLog(@"4");
      });
      NSLog(@"5");
  }
  return 0;
}

上面這段代碼的輸出結果?

輸出結果:12345。同步方式提交任務,無論在串行隊列還是并發隊列都會在當前線程中執行。因為是同步,所以都是在主線程執行。globaQueue 是并發隊列,所以不會造成死鎖。如果將 倆個globaQueue 都換成串行隊列,就會造成死鎖.

1.1.3 異步 + 串行

-(void)viewDidLoad
{
    dispatch_async(dispatch_get_main_queue(), ^{
       
        [self doSomething];
    });
}

1.1.4 異步 + 并發

面試題

- (void)viewDidLoad {
  [super viewDidLoad];
  dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

  dispatch_async(global_queue, ^{
      NSLog(@"1");
      [self performSelector:@selector(printLog) withObject:nil afterDelay:0];
      NSLog(@"3");
  });
}
- (void)printLog {
  NSLog(@"2");
}

上述代碼執行的結果?

結果:13。提交異步任務到并發隊列,任務調用了performSelector:withObject:afterDelay:。由于GCD提交的任務是在某個子線程中執行,子線程沒有RunLoop。由于performSelector:withObject:afterDelay:需要RunLoop才可生效,所以方法不執行。這個問題,考察performSelector:withObject:afterDelay:內部實現。
分析首先這是一個異步方式分配到全局并發隊列當中的,這個 Block 會在 GCD 底層的線程池當中的某一個線程中執行。 GCD 底層所分派的這些線程默認是不開啟 Runloop 的,而 performSelector 方法是需要創建相應的任務提交到 Runloop 上,所以在GCD 底層線程沒有 Runloop 的情況下,這個方法就會失效 .也就是說 performSelector要想能夠有效執行必須是它方法調用所屬的當前線程有 Runloop 的。

任務和隊列示例代碼:任務和隊列Demo包含面試題講解

二、多讀單寫解決方案

  • pthread_rwlock: 讀寫鎖

  • dispatch_barrier_async() 異步柵欄調用

怎么利用GCD實現多讀單寫?或者如何實現多讀單寫?

2.1 什么是多讀單寫?

讀者和讀者,并發。 讀者和寫者,互斥。 寫者與寫者,互斥。


904629-13d27298ba91ffe0.png

2.2 解決方法

  dispatch_async(global_queue, ^{
      NSLog(@"讀取1");
  });
  dispatch_async(global_queue, ^{
      NSLog(@"讀取2");
  });
  dispatch_barrier_async(global_queue, ^{
      NSLog(@"寫入1");
  });
  dispatch_async(global_queue, ^{
      NSLog(@"讀取3");
  });
  dispatch_async(global_queue, ^{
      NSLog(@"讀取4");
  });

dispatch_barrier_async函數會等待追加到并發隊列上的并行執行的處理全部結束之后,在將指定的處理追加到該并發隊列中。然后等dispatch_barrier_async函數追加的處理執行完畢后,并發隊列才恢復為一般的動作,追加到并發隊列的處理又開始并行執行。

三、dispatch_group_async()

面試題:

如何用GCD 實現:A、B、C三個任務并發,完成后執行任務D?

實現追加到并發隊列中的多個任務全部結束后再執行想執行的任務。無論向什么樣的隊列中追加處理,使用DispatchGroup都可監視這些處理執行的結束。一旦檢測到所有處理執行結束,該Dispatch Group與隊列相同。

dispatch_group_async() 同dispatch_async()函數相同,都追加Block到指定的DispatchQueue中。當組中所有任務都執行完成后,dispatch_group_notify()執行Block中的內容。

示例代碼:

// dispatch_group_notify
- (void)test {
  dispatch_group_t group = dispatch_group_create();
  dispatch_queue_t queue = dispatch_queue_create("com.lqq.queue", DISPATCH_QUEUE_CONCURRENT);
  dispatch_group_async(group, queue, ^{
      for (int i = 0; i < 5; i++) {
          NSLog(@"任務1 - %@", [NSThread currentThread]);
      }
  });
  dispatch_group_async(group, queue, ^{
      for (int i = 0; i < 5; i++) {
          NSLog(@"任務2 - %@", [NSThread currentThread]);
      }
  });
  //--------------示例1-------------------       // 寫法一, 等上面的任務執行完成后,才會在主隊列中執行任務3
//   dispatch_group_notify(group, queue, ^{
//       dispatch_async(dispatch_get_main_queue(), ^{
//           for (int i = 0; i < 5; i++) {
//               NSLog(@"任務3 - %@", [NSThread currentThread]);
//           }
//       });
//   });

  //寫法二:直接在主隊列中執行
//   dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//       for (int i = 0; i < 5; i++) {
//           NSLog(@"任務3 - %@", [NSThread currentThread]);
//       }
//   });
  //--------------示例2-------------------       // 如果有多個notify會怎么執行呢?
  dispatch_group_notify(group, queue, ^{
      for (int i = 0; i < 5; i++) {
          NSLog(@"任務3 - %@", [NSThread currentThread]);
      }
  });
  dispatch_group_notify(group, queue, ^{
      for (int i = 0; i < 5; i++) {
          NSLog(@"任務4 - %@", [NSThread currentThread]);
      }
  });
  // 任務3和任務4是交替執行的
}

另外,也可以使用dispatch_group_wait,如下:

  // 監控任務是否完成,當完成時會返回0,不完成一直等待。
  long result = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
  if (result == 0) {
      NSLog(@"全部任務執行完成");
  }

線程組示例代碼:線程組API使用Demo

四、NSOperation

需要和NSOperationQueue配合使用來實現多線程。優勢和特點:添加任務依賴、任務執行狀態控制、控制最大并發量。

任務狀態的控制

isReady
isExecuting
isFinished
isCanceled

如果重寫main方法,底層控制變更任務執行完成狀態,以及任務退出。

如果重寫start方法,自行控制任務狀態。

- (void) start
{
  NSAutoreleasePool *pool = [NSAutoreleasePool new];
  double        prio = [NSThread  threadPriority];

  AUTORELEASE(RETAIN(self));    // Make sure we exist while running.
  [internal->lock lock];
  NS_DURING
    {
      if (YES == [self isExecuting])
    {
      [NSException raise: NSInvalidArgumentException
              format: @"[%@-%@] called on executing operation",
        NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
    }
      if (YES == [self isFinished])
    {
      [NSException raise: NSInvalidArgumentException
              format: @"[%@-%@] called on finished operation",
        NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
    }
      if (NO == [self isReady])
    {
      [NSException raise: NSInvalidArgumentException
              format: @"[%@-%@] called on operation which is not ready",
        NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
    }
      if (NO == internal->executing)
    {
      [self willChangeValueForKey: @"isExecuting"];
      internal->executing = YES;
      [self didChangeValueForKey: @"isExecuting"];
    }
    }
  NS_HANDLER
    {
      [internal->lock unlock];
      [localException raise];
    }
  NS_ENDHANDLER
  [internal->lock unlock];

  NS_DURING
    {
      if (NO == [self isCancelled])
    {
      [NSThread setThreadPriority: internal->threadPriority];
      [self main];
    }
    }
  NS_HANDLER
    {
      [NSThread setThreadPriority:  prio];
      [localException raise];
    }
  NS_ENDHANDLER;

  [self _finish];
  [pool release];
}

面試題

系統是怎樣移除一個isFinished=YES的NSOperation的?答案:KVO

五、NSThread

啟動流程


image.png

start() -> 創建pthread -> main() ->[target performSelector:selector] -> exit()

- (void) start
{
  pthread_attr_t    attr;

  if (_active == YES)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-%@] called on active thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }
  if (_cancelled == YES)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-%@] called on cancelled thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }
  if (_finished == YES)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-%@] called on finished thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }

  /* Make sure the notification is posted BEFORE the new thread starts.
   */
  gnustep_base_thread_callback();

  /* The thread must persist until it finishes executing.
   */
  RETAIN(self);

  /* Mark the thread as active while it's running.
   */
  _active = YES;

  errno = 0;
  pthread_attr_init(&attr);
  /* Create this thread detached, because we never use the return state from
   * threads.
   */
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
  /* Set the stack size when the thread is created.  Unlike the old setrlimit
   * code, this actually works.
   */
  if (_stackSize > 0)
    {
      pthread_attr_setstacksize(&attr, _stackSize);
    }
  if (pthread_create(&pthreadID, &attr, nsthreadLauncher, self))
    {
      DESTROY(self);
      [NSException raise: NSInternalInconsistencyException
                  format: @"Unable to detach thread (last error %@)",
                  [NSError _last]];
    }
}

/**
 * Trampoline function called to launch the thread
 */
static void *
nsthreadLauncher(void *thread)
{
  NSThread *t = (NSThread*)thread;

  setThreadForCurrentThread(t);

  /*
   * Let observers know a new thread is starting.
   */
  if (nc == nil)
    {
      nc = RETAIN([NSNotificationCenter defaultCenter]);
    }
  [nc postNotificationName: NSThreadDidStartNotification
            object: t
          userInfo: nil];

  [t _setName: [t name]];

  [t main];

  [NSThread exit];
  // Not reached
  return NULL;
}

- (void) main
{
  if (_active == NO)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-%@] called on inactive thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }

  [_target performSelector: _selector withObject: _arg];
}

如何實現常駐進程? 通過使用NSThread和RunLoop實現。

在iOS多線程中,經常會出現資源競爭和死鎖的問題。本節將學習iOS中不同的鎖。

線程同步方案

常見的兩個問題:多線程買票和存取錢問題。

示例:存取錢問題

// 示例:存取錢問題
- (void)moneyTest {
  self.moneyCount = 100;

  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  dispatch_async(queue, ^{
      for (int i = 0; i < 5; i++) {
          [self saveMoney];
      }
  });
  dispatch_async(queue, ^{
      for (int i = 0; i < 5; i++) {
          [self takeMoney];
      }
  });
}
- (void)saveMoney {
  int oldCount = self.moneyCount;
  sleep(0.2);
  oldCount += 50;
  self.moneyCount = oldCount;
  NSLog(@"存50,還剩%d錢", self.moneyCount);
}
?
- (void)takeMoney {
  int oldCount = self.moneyCount;
  sleep(0.2);
  oldCount -= 20;
  self.moneyCount = oldCount;
  NSLog(@"取20,還剩%d錢", self.moneyCount);
}

示例:賣票問題

// 示例:買票
- (void)sellTest {
  self.count = 15;
  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  dispatch_async(queue, ^{
      for (int i = 0; i < 5; i++) {
          [self printTest2];
      }
  });
  dispatch_async(queue, ^{
      for (int i = 0; i < 5; i++) {
          [self printTest2];
      }
  });
  dispatch_async(queue, ^{
      for (int i = 0; i < 5; i++) {
          [self printTest2];
      }
  });
}
- (void)printTest2 {
  NSInteger oldCount = self.count;
  sleep(0.2);
  oldCount --;
  self.count = oldCount;
  NSLog(@"還剩%ld張票 - %@", (long)oldCount, [NSThread currentThread]);
}

解決上面這種資源共享問題,就需要使用線程同步技術。線程同步技術的核心是:鎖。下面學習iOS中不同鎖的使用,比較不同鎖之間的優缺點。

示例代碼:演示購票和存取錢問題:Demo

iOS當中有哪些鎖?

@synchronized 常用于單例
atomic 原子性
OSSpinLock 自旋鎖
NSRecursiveLock 遞歸鎖
NSLock
dispatch_semaphore_t 信號量
NSCondition 條件
NSConditionLock 條件鎖</pre>

簡介:

  • @synchronized 使用場景:一般在創建單例對象時使用,保證對象在多線程中是唯一的。

  • atomic 屬性關鍵字原子性,保證賦值操作是線程安全的,讀取操作不能保證線程安全。

  • OSSpinLock 自旋鎖。特點:循環等待訪問,不釋放當前資源。常用于輕量級數據訪問,簡單的int值+1/-1操作。 一般用于引用計數操作

  • NSLock 某個線程A調用lock方法。這樣,NSLock將被上鎖。可以執行“關鍵部分”,完成后,調用unlock方法。如果,在線程A 調用unlock方法之前,另一個線程B調用了同一鎖對象的lock方法。那么,線程B只有等待。直到線程A調用了unlock。

 [lock lock]; //加鎖
// 關鍵部分代碼{}
[lock unlock]; // 解鎖
image.png

NSLock使用的過程要注意因重復上鎖產生的死鎖

  • NSRecursiveLock 遞歸鎖,特點:遞歸鎖在被同一線程重復獲取時不會產生死鎖。

  • dispatch_semaphore_t 信號量

image.png
image.png
image.png
    // 創建信號量結構體對象,含有一個int成員
    dispatch_semaphore_create(1);
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);// 先對value減一,如果小于零表示沒有資源可以訪問。通過主動行為進行阻塞。
    dispatch_semaphore_signal(semaphore);// value加1,小于等零表示有隊列在排隊,通過被動行為進行喚醒。        

OSSpinLock

自旋鎖,等待鎖的線程會處于忙等狀態,一直占用著CPU資源。

常用API:

導入頭文件
#import <libkern/OSAtomic.h>
// 初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
// 嘗試加鎖
OSSpinLockTry(&lock);
// 加鎖
OSSpinLockLock(&lock);
// 解鎖
OSSpinLockUnlock(&lock);

使用OSSpinLock解決賣票問題

// 自旋鎖:#import <libkern/OSAtomic.h>
// 定義一個全局的自旋鎖對象 lock 。
- (void)printTest2 {
  // 加鎖
  OSSpinLockLock(&_lock);
  NSInteger oldCount = self.count;
  sleep(0.2);
  oldCount --;
  self.count = oldCount;
  NSLog(@"還剩%ld張票 - %@", (long)oldCount, [NSThread currentThread]);
  // 解鎖
  OSSpinLockUnlock(&_lock);
}

使用OSSpinLock解決存取錢問題

- (void)saveMoney {
  OSSpinLockLock(&_moneyLock);
  int oldCount = self.moneyCount;
  sleep(0.2);
  oldCount += 50;
  self.moneyCount = oldCount;
  NSLog(@"存50,還剩%d錢", self.moneyCount);
  OSSpinLockUnlock(&_moneyLock);
}
- (void)takeMoney {
  OSSpinLockLock(&_moneyLock);
  int oldCount = self.moneyCount;
  sleep(0.2);
  oldCount -= 20;
  self.moneyCount = oldCount;
  NSLog(@"取20,還剩%d錢", self.moneyCount);
  OSSpinLockUnlock(&_moneyLock);
}

注意:賣票和取錢不要共用一把鎖。這里創建了兩把鎖sellLockmoneyLock

自旋鎖現在不再安全,因為可能出現優先級反轉問題。如果等待鎖的線程優先級較高,他會一直占用CPU資源,優先級低的線程就無法獲取CPU資源完成任務并釋放鎖。可以查看這篇文章不再安全的OSSpinLock

本節示例代碼:線程同步解決方案Demo

os_unfair_lock

自旋鎖已經不再安全,存在優先級反轉問題。蘋果在iOS10開始使用os_unfair_lock取代了OSSpinLock。從底層調用來看,自旋鎖和os_unfair_lock的區別,前者等待線程處于忙等,而后者等待線程處于休眠狀態。

常用API:

導入頭文件 
#import <os/lock.h>
// 初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
// 嘗試加鎖
os_unfair_lock_trylock(&_lock);
// 加鎖
os_unfair_lock_lock(&_lock);
// 解鎖
os_unfair_lock_unlock(&_lock);

pthread_mutex

互斥鎖,等待鎖的線程處于休眠狀態。

常用API:

// 頭文件 #import <pthread.h>
- (void)__initLock:(pthread_mutex_t *)lock {
  // 初始化屬性
  pthread_mutexattr_t attr;
  pthread_mutexattr_init(&attr);
  // 設置為普通鎖,PTHREAD_MUTEX_RECURSIVE表示遞歸鎖
  pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
  // 初始化鎖
  pthread_mutex_init(lock, &attr);
  // 銷毀屬性
  pthread_mutexattr_destroy(&attr);
}
// 加鎖
pthread_mutex_lock(&lock);
// 解鎖
pthread_mutex_unlock(&lock);
// 初始化條件
pthread_cond_init(&cond, NULL)
// 等待條件(進入休眠,放開鎖;被喚醒后,會再次加鎖)
pthread_cond_wait(&cond, &lock);
// 激活一個等待該條件的線程
pthread_cond_signal(&cond);
// 激活所有等待該條件的線程
pthread_cond_broadcast(&cond); 
// 銷毀資源
pthread_mutex_destory(&lock);
pthread_cond_destory(&cond);

其中PTHREAD_MUTEX_DEFAULT設置的是鎖的類型,還有另一種類型PTHREAD_MUTEX_RECURSIVE表示遞歸鎖。遞歸鎖允許同一個線程對一把鎖進行重復加鎖。

NSLock&NSRecursiveLock&NSCondition

NSLock是對mutex普通鎖的封裝。

@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
?
@interface NSLock : NSObject <NSLocking> {
- (BOOL)tryLock; // 嘗試加鎖
- (BOOL)lockBeforeDate:(NSDate *)limit; //在時間之前獲取鎖并返回,YES表示成功。
}
@end

NSRecursiveLock是對mutex遞歸鎖的封裝,API同NSLock相似。

NSCondition是對mutex條件的封裝。

@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSCondition : NSObject <NSLocking> {
- (void)wait; // 等待
- (BOOL)waitUntilDate:(NSDate *)limit; // 等待某一個時間段
- (void)signal; // 喚醒
- (void)broadcast; // 喚醒所有睡眠線程
}

以上可以查看pthread_mutex使用。

atomic

atomic用于保證屬性settergetter的原子性操作,相當于對settergetter內部加了同步鎖。它并不能保證使用屬性的使用過程是線程安全的。

NSConditionLock

NSConditionLock是對NSCondition的進一步封裝。可以設置具體的條件值。

// 遵循NSLocking協議。
@interface NSConditionLock : NSObject <NSLocking> {
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER; // 初始化,傳入一個條件值
?
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition; // 條件值符合加鎖
- (BOOL)tryLock; //嘗試加鎖
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@end

示例代碼:

// 刪除
- (void)__one {
?
  // 當鎖內部條件值為1時,加鎖。
//   [self.condition lockWhenCondition:1];
  [self.condition lock]; // 直接使用lock也可以
  sleep(1);
  NSLog(@"%s ①", __func__);
  [self.condition unlockWithCondition:2]; // 解鎖,并且條件設置為2
}
// 添加
- (void)__two {

  [self.condition lockWhenCondition:2]; //條件值為2時,加鎖。
  sleep(1);
  NSLog(@"%s ②", __func__);
  [self.condition unlockWithCondition:3];
}
?
// 添加
- (void)__three {

  [self.condition lockWhenCondition:3]; //條件值為3時,加鎖。
  sleep(1);
  NSLog(@"%s ③", __func__);
  [self.condition unlock];
}
?
- (void)otherTest {
  // ①
  [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
  // ②
  [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
  // ③
  [[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start];

  // 通過設置條件值,可以決定線程的執行順序。
}

輸出結果:

-[LENSConditionLock __one] ①
-[LENSConditionLock __two] ②
-[LENSConditionLock __three] ③

信號量

常用API:

// 初始化
dispatch_semaphore_t semaphore = dispatch_semaphore_create(5);  
// 如果信號量的值<=0,當前線程就會進入休眠等待,直到信號量的值>0
// 如果信號量的值>0,就減1,然后往下執行后面的代碼
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
// 讓信號量的值增加1,信號量值不等于零時,前面的等待的代碼會執行。
dispatch_semaphore_signal(self.semaphore);

dispatch_semaphore信號量的初始值,控制線程的最大并發訪問數量。 信號量的初始值為1,代表同時只允許1條線程訪問資源,保證線程同步。

示例代碼:

// 設置信號量初始值為5。
- (void)otherTest {
  for (int i = 0; i < 20; i ++) {
      [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
  }
}
- (void)test {
  // 如果信號量的值<=0,當前線程就會進入休眠等待,直到信號量的值>0
  // 如果信號量的值>0,就減1,然后往下執行后面的代碼
  dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
  sleep(2);
  NSLog(@"test - %@", [NSThread currentThread]);
  // 讓信號量的值增加1
  dispatch_semaphore_signal(self.semaphore);
}

@synchronized

@synchronized是對mutex遞歸鎖的封裝。 不推薦使用,性能比較差。

// 源碼:objc4中的objc-sync.mm
@synchronized (obj) {
}

性能比較

不再安全的OSSpinLock中對比了不同鎖的性能。 推薦使用dispatch_semaphorepthread_mutex兩個。因為OSSpinLock性能最好但是不安全,os_unfair_lock在iOS10才出現低版本不支持不推薦。

自旋鎖、互斥鎖的選擇

自旋鎖預計線程等待鎖的時間很短,加鎖經常被調用但競爭情況很少出現。常用于多核處理器。 互斥鎖預計等待鎖的時間較長,單核處理器。臨界區有IO操作,例如文件讀寫。

示例代碼:鎖實例代碼-Github

小結

  • 怎樣用GCD實現多讀單寫?

  • iOS提供幾種多線程技術各自的特點?

  • NSOperation對象在Finished之后是怎樣從隊列中移除的?

  • 你都用過哪些鎖?結合實際談談你是怎樣使用的?

參考

小碼哥底層班視頻 正確使用多線程同步鎖@synchronized() 深入理解iOS開發中的鎖 Object-C 多線程中鎖的使用-NSLock

附錄

GCD API介紹:
dispatch_queue_create(名稱,類型)
類型為NULL時,為串行隊列;為DISPATCH_QUEUE_CONCUREENT,為并行隊列.。
?
需要手動release釋放隊列
dispatch_release(隊列)
?
主隊列,串行
dispatch_get_main_queue()
dispatch_get_global_queue(優先級,0)
?
優先級有四種情況:
  高,默認,低,后臺

為自己創建的隊列設置優先級
?
dispatch_set_target_queue(自定義隊列,其他已知優先級的隊列) 
?
dispatch_after(時間,隊列,Block)
時間:dispatch_time_t 
?
dispatch_group 監聽所有任務結束。
?
dispatch_group_create() //創建一個隊列組,需要手動release
dispatch_group_async(組,隊列,block)
dispatch_group_notify(組,隊列,block) // 所有任務都結束后調用
也可以使用dispatch_group_wait()
?
dispatch_barrier_async() 一般用于數據庫操作和文件讀寫。
?
同步任務
dispatch_sync(隊列,block) 
?
異步任務
dispatch_async(隊列,block)
?
指定執行次數
dispatch_apply(次數,隊列,block)
掛起
dispatch_suspend(隊列)
?
恢復
dispatch_resume(隊列)
?
信號量
Dispatch Semaphore是持有計數的信號,該計數是多線程編程中的計數類型信號。
?
dispatch_semaphore_create(技術值)
dispatch_semaphore_wait(semaphore, 時間)
dispatch_semaphore_signal(semaphore)
dispatch_reliease(semaphore)
?
// 示例
// 創建一個對象,默認的計數值為0,等待。當計數值大于1或者等于1時,減去1而不等待。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// 發送計數值加一信號
dispatch_semaphore_signal(semaphore);
// 等待計數值變化,第二個參數是等待時間,這里是永久等待。
// 當計數值大于0時,返回0。等于0不會返回任何值。
long result = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
?
if (result == 0) {
  NSLog(@"執行排他性操作");
}
dispatch_once函數是保證在應用程序執行中只執行一次的API。
dispatch I/O 分割讀取,提高讀取效率。
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容