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阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容