美文网首页
iOS 多线程

iOS 多线程

作者: NapoleonY | 来源:发表于2020-07-27 15:41 被阅读0次

    iOS 多线程有几种方式

    • GCD
    • NSOpeartion
    • NSThread
    • phread

    多线程

    GCD
    1. dispatch_once_t

      + (TestModel *)shared {
          static TestModel *model;
          static dispatch_once_t once;
          dispatch_once(&once, ^{
              model = [[TestModel alloc] init];
          });
          return model;
      }
      
    2. dispatch_group

      - (void)dispatchGroup {
          dispatch_queue_t queue = dispatch_queue_create("groupTest", DISPATCH_QUEUE_CONCURRENT);
          dispatch_group_t group = dispatch_group_create();
      
          dispatch_group_async(group, queue, ^{
              NSLog(@"任务1 ready");
              sleep(2);
              NSLog(@"任务1 完成");
          });
           dispatch_group_enter(group);
          dispatch_async(queue, ^{
              NSLog(@"任务2 ready");
              sleep(4);
              NSLog(@"任务2 完成");
              dispatch_group_leave(group);
          });
          dispatch_group_notify(group, queue, ^{
              NSLog(@"group 任务完成");
          });
      }
      
      • dispatch_group_notify 可以添加多次,并会多次调用
      • 只要 group 中的任务没有完成,group 完成的监听就不会被调用,即使是后追加的任务
      • notify 方法中第二个 queue 的参数决定了 callBack 将会在那个队列执行
    3. dispatch_apply 多线程快速遍历

      • 本质是 dispatch_syncdispatch_group 关联的 api,因为该方法会等待内部所有操作都结束再返回,内部操作是否同步依赖传入的queue,外部必定是同步的。如果有需要,需将该方法放到一个异步的并行队列中

      • 如果传入多线程,输出的下标未必按照顺序执行

      - (void)dispatchApply {
          dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
          dispatch_apply(10, queue, ^(size_t index) {
              NSLog(@"%@------%d", NSThread.currentThread, index);
              sleep(1);
          });
           NSLog(@"111111");
      }
      // "111111" 必定出现在最后
      
    4. 定时器 DispatchSourceTimer

      不同于基于 Runloop 的 NSTimerDispatchSourceTimer 不会因为子线程没有正在运行的 Runloop 而失效,也不会有循环引用、计时不准(每次 runloop 循环才会检查定时器是否需要被执行)等问题。但有几点需要注意:

      • suspend 与 resume 一定要成对使用,否则会 crash
      • timer 最好被持有,否则在 suspend 时可能 crash
      var timer: DispatchSourceTimer?
      var index: Int = 0
      func startTimer() {
          timer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.global())
          timer?.schedule(deadline: .now(), repeating: 0.1)
          timer?.setEventHandler(handler: {
              print("index: \(index)")
              index += 1
              if index == 10 {
                  cancelTimer()
              }
          })
          timer?.resume()
      }
      
      private func cancelTimer() {
          timer?.cancel()
          timer = nil
      }
      
    5. 信号量 DispatchSemaphore

      信号量类似于锁,信号量为 0 则阻塞线程,大于 0 则不会阻塞。因此可以通过改变信号量的值来控制是否阻塞线程

      • DispatchSemaphore(value: 0) 初始化
      • semaphore.signal() // 信号量 +1
      • semaphore.wait() 在信号量大于 0 的前提下,信号量 -1,如果信号量本来为 0,则线程休眠,加入到等待这个信号的线程队列当中。当信号量大于 0 时,就会唤醒这个等待队列中靠前的线程,继续线程后面的代码且对信号量减 1,也就确保了信号量大于 0 才减 1,所以不存在信号量小于 0 的情况(除非在初始化时设置为负数,不过这样做应用程序会 crash)。
      // 下载两个图片后执行操作 A
      // 1. 初始化信号量为 0
      var seamphore = DispatchSemaphore(value: -0)
      // 2. 操作 A 前添加两个 wait 操作
      seamphore.wait()
      seamphore.wait()
      // 3. 在每个下载结果回调中添加
      seamphore.signal()
      
    6. 线程栅栏 dispatch_barrier

      线程栅栏可以阻塞某个 queue(必须是自定义的并行 queue,如果是 global ,则不会有预期效果)中任务的执行直到 queue 中栅栏之前的任务执行完毕

      - (void)dispatchBarrier {
          dispatch_queue_t queue = dispatch_queue_create("aaa", DISPATCH_QUEUE_CONCURRENT);
          for (int i = 1; i <= 3; i ++) {
              dispatch_async(queue, ^{
                  sleep(1);
                  NSLog(@"%d 任务结束", i);
              });
          }
          NSLog(@"栅栏前面");
          dispatch_barrier_sync(queue, ^{
              sleep(3);
              NSLog(@"栅栏结束");
          });
          NSLog(@"栅栏后面");
          for (int i = 4; i <= 6; i ++) {
              dispatch_async(queue, ^{
                  sleep(1);
                  NSLog(@"%d 任务结束", i);
              });
          }
          NSLog(@"代码结束");
      }
      

      运行结果

      dispatch_barrier_sync 运行结果
      2020-07-24 20:17:09.905062+0800 ObjcTest[13685:969878] 栅栏前面
      2020-07-24 20:17:10.907255+0800 ObjcTest[13685:969907] 2 任务结束
      2020-07-24 20:17:10.907310+0800 ObjcTest[13685:969908] 1 任务结束
      2020-07-24 20:17:10.907579+0800 ObjcTest[13685:969906] 3 任务结束
      2020-07-24 20:17:13.907888+0800 ObjcTest[13685:969878] 栅栏结束
      2020-07-24 20:17:13.908349+0800 ObjcTest[13685:969878] 栅栏后面
      2020-07-24 20:17:13.908954+0800 ObjcTest[13685:969878] 代码结束
      2020-07-24 20:17:14.913851+0800 ObjcTest[13685:969906] 4 任务结束
      2020-07-24 20:17:14.914364+0800 ObjcTest[13685:969908] 5 任务结束
      2020-07-24 20:17:14.914628+0800 ObjcTest[13685:969907] 6 任务结束
      dispatch_barrier_async 运行结果
      2020-07-24 20:18:16.955921+0800 ObjcTest[13691:970322] 栅栏前面
      2020-07-24 20:18:16.956002+0800 ObjcTest[13691:970322] 栅栏后面
      2020-07-24 20:18:16.956040+0800 ObjcTest[13691:970322] 代码结束
      2020-07-24 20:18:17.961793+0800 ObjcTest[13691:970352] 1 任务结束
      2020-07-24 20:18:17.962022+0800 ObjcTest[13691:970353] 2 任务结束
      2020-07-24 20:18:17.963561+0800 ObjcTest[13691:970357] 3 任务结束
      2020-07-24 20:18:20.967381+0800 ObjcTest[13691:970357] 栅栏结束
      2020-07-24 20:18:21.974364+0800 ObjcTest[13691:970357] 4 任务结束
      2020-07-24 20:18:21.974901+0800 ObjcTest[13691:970353] 5 任务结束
      2020-07-24 20:18:21.975174+0800 ObjcTest[13691:970352] 6 任务结束
      

      dispatch_barrier_syncdispatch_barrier_async 区别为

      • 同步栅栏会阻塞之后的普通代码的执行,异步栅栏则不会

      应用线程栅栏的特性,可以更好的做一些线程同步。例如

      任务 1、2、3结束后需要执行任务 4、5、6

      如果用 dispatch_group ,则将任务 1、2、3 添加到 group 中,在 dispatch_group_notify 中执行任务 4、5、6

    NSOperation

    NSOperation 是苹果 GCD 面向对象的封装

    优点:

    • 添加操作间的依赖关系,控制执行顺序

    • 可以方便的取消一个操作的执行

    • 设定操作的优先级

    • 使用 KVO 观察操作执行状态的更改

    1. NSOperation 执行的方式

      • 添加到 NSOperationQueue,系统会自动将 NSOperationQueue 中的 NSOperation 取出来,在线程中执行操作
      • 如果不显式将 NSOperation 添加到 NSOperationQueue 中,也可以调用 operation.start() 方法,操作会在当前线程中执行

      备注:使用 start 方法时,如果 NSBlockOperation 调用 addExecutionBlock:方法,添加额外的操作,包括 blockOperationWithBlock 中的操作在内的这些操作可能在不同的线程中并发执行,具体由系统决定

          NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
              for (int i = 0; i < 3; i ++) {
                  sleep(2);
                  NSLog(@"1---%@", NSThread.currentThread);
              }
          }];
          [op addExecutionBlock:^{
              for (int i = 0; i < 3; i ++) {
                  sleep(2);
                  NSLog(@"2---%@", NSThread.currentThread);
              }
          }];
          [op addExecutionBlock:^{
              for (int i = 0; i < 3; i ++) {
                  sleep(2);
                  NSLog(@"3---%@", NSThread.currentThread);
              }
          }];
          [op addExecutionBlock:^{
              for (int i = 0; i < 3; i ++) {
                  sleep(2);
                  NSLog(@"4---%@", NSThread.currentThread);
              }
          }];
          [op addExecutionBlock:^{
              for (int i = 0; i < 3; i ++) {
                  sleep(2);
                  NSLog(@"5---%@", NSThread.currentThread);
              }
          }];
          [op addExecutionBlock:^{
              for (int i = 0; i < 3; i ++) {
                  sleep(2);
                  NSLog(@"6---%@", NSThread.currentThread);
              }
          }];
          [op start];
      
      2020-07-27 14:19:25.237520+0800 ObjcTest[19476:1527007] 1---<NSThread: 0x282870ac0>{number = 5, name = (null)}
      2020-07-27 14:19:25.237532+0800 ObjcTest[19476:1526976] 2---<NSThread: 0x28283da80>{number = 1, name = main}
      2020-07-27 14:19:27.239170+0800 ObjcTest[19476:1526976] 2---<NSThread: 0x28283da80>{number = 1, name = main}
      2020-07-27 14:19:27.239164+0800 ObjcTest[19476:1527007] 1---<NSThread: 0x282870ac0>{number = 5, name = (null)}
      2020-07-27 14:19:29.240627+0800 ObjcTest[19476:1526976] 2---<NSThread: 0x28283da80>{number = 1, name = main}
      2020-07-27 14:19:29.240620+0800 ObjcTest[19476:1527007] 1---<NSThread: 0x282870ac0>{number = 5, name = (null)}
      2020-07-27 14:19:31.242202+0800 ObjcTest[19476:1527007] 4---<NSThread: 0x282870ac0>{number = 5, name = (null)}
      

      从结果中可以看出 blockOperationWithBlock 中的代码不是在主线程中执行的,并且系统一共开启了两个线程执行该 Operation

    2. NSOperationQueue 控制串行、并发

      maxConcurrentOperationCount 属性可以控制一个队列中可以有多少个操作同时参与并发执行

      注意:这里的 maxConcurrentOperationCount 控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。而且一个操作也并非只能在一个线程中运行。

    3. NSOperation 操作依赖

      NSOperation 可以添加操作之间的依赖。因此我们可以很方便的控制操作之间的执行先后顺序。并且一个操作可以添加多个依赖。

      例如我们有如下需求:A、B两个操作,A 执行完毕,B 才能执行操作。

      // 1.创建队列
      NSOperationQueue *queue = [[NSOperationQueue alloc] init];
      
      // 2.创建操作
      NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
          for (int i = 0; i < 2; i++) {
               [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
               NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
          }
      }];
      NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
          for (int i = 0; i < 2; i++) {
              [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
              NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
          }
      }];
      
      // 3.添加依赖
      [op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2
      
      // 4.添加操作到队列中
      [queue addOperation:op1];
      [queue addOperation:op2];
      

      输出如下

      2020-07-27 14:43:35.998300+0800 ObjcTest[19617:1533985] 1---<NSThread: 0x281496200>{number = 6, name = (null)}
      2020-07-27 14:43:38.000611+0800 ObjcTest[19617:1533985] 1---<NSThread: 0x281496200>{number = 6, name = (null)}
      2020-07-27 14:43:40.007131+0800 ObjcTest[19617:1533985] 2---<NSThread: 0x281496200>{number = 6, name = (null)}
      2020-07-27 14:43:42.021944+0800 ObjcTest[19617:1533985] 2---<NSThread: 0x281496200>{number = 6, name = (null)}
      

      可以看出,op1 先执行完毕后,op2 才开始执行

    4. NSOperation 优先级

      优先级只体现在两个时间点

      • 依赖任务处理完成、队列对后续任务的调度
      • 依赖队列从暂停转变为重新启动、后续任务的调度
      NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
              NSLog(@"1--- 开始执行, %@", NSThread.currentThread);
              sleep(2);
              NSLog(@"1--- 执行完毕");
          }];
          op1.queuePriority = NSOperationQueuePriorityLow;
      
          NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
              NSLog(@"2--- 开始执行, %@", NSThread.currentThread);
              sleep(2);
              NSLog(@"2--- 执行完毕");
          }];
          op2.queuePriority = NSOperationQueuePriorityHigh;
      
          NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
              NSLog(@"依赖--- 开始执行, %@", NSThread.currentThread);
              sleep(2);
              NSLog(@"依赖--- 执行完毕");
          }];
          [op1 addDependency:op3];
          [op2 addDependency:op3];
          NSOperationQueue *queue = [[NSOperationQueue alloc] init];
          queue.maxConcurrentOperationCount = 1;
          [queue addOperation:op1];
          [queue addOperation:op2];
          [queue addOperation:op3];
      

      结果如下

      2020-07-27 15:13:05.623639+0800 ObjcTest[19750:1540177] 依赖--- 开始执行, <NSThread: 0x281561c00>{number = 5, name = (null)}
      2020-07-27 15:13:07.629611+0800 ObjcTest[19750:1540177] 依赖--- 执行完毕
      2020-07-27 15:13:07.630799+0800 ObjcTest[19750:1540177] 2--- 开始执行, <NSThread: 0x281561c00>{number = 5, name = (null)}
      2020-07-27 15:13:09.631681+0800 ObjcTest[19750:1540177] 2--- 执行完毕
      2020-07-27 15:13:09.632164+0800 ObjcTest[19750:1540176] 1--- 开始执行, <NSThread: 0x281561380>{number = 3, name = (null)}
      2020-07-27 15:13:11.637458+0800 ObjcTest[19750:1540176] 1--- 执行完毕
      
    5. NSOperationQueue 暂停和取消

      注意:

      1. 这里的暂停和取消(包括操作的取消和队列的取消)并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕后不再执行新的操作
      2. 暂停和取消的区别在于:暂停之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,不再执行剩下的操作
    NSThread

    NSThread 在代码中偶尔会使用,例如 [NSThread currentThread]

    1. 创建、启动线程

      - (void)threadTest {
          NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
          [thread start];
        // 创建线程后自动启动线程
        //[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
      
        // 隐式创建并启动线程
        //[self performSelectorInBackground:@selector(run) withObject:nil];
      }
      
      - (void)run {
          NSLog(@"currentThread: %@", NSThread.currentThread);
      }
      
    2. 线程状态控制

      + (void)sleepUntilDate:(NSDate *)date;
      + (void)sleepForTimeInterval:(NSTimeInterval)ti;// 线程进入阻塞状态
      + (void)exit; // 线程 kill
      
    3. 线程间通信

      - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
      - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
      
    4. 与 runloop 关系

      - (void)run {
          NSLog(@"currentThread: %@", NSThread.currentThread);
          [self performSelector:@selector(threadAfter) withObject:nil afterDelay:1];
      }
      
      - (void)threadAfter {
          NSLog(@"1111111");
      }
      

      上述代码默认不会起作用,‘1111111’ 不会被打印出来。 performSelector: withObject: afterDelay: 方法默认会创建一个 timer 添加到当前 runloop 中,而子线程默认不开启 runloop,因此上述代码不起作用。
      解决方法是在 performSelector 方法后面添加[[NSRunLoop currentRunLoop] run];
      如果将 runloop 启动的代码放到前面,仍然不会起作用,原因是 runloop 启动后没有可执行的代码,会立刻退出,此时再添加 timer 也没有什么作用。

    pthread

    pthread 是一套通用的多线程 API,使用 C 语言编写,需要程序员自己管理线程的生命周期,使用难度较大,很少使用

    • pthread_create()创建一个线程
    • pthread_exit()终止当前线程
    • pthread_cancel()中断另外一个线程的运行
    • pthread_join()阻塞当前的线程,直到另外一个线程运行结束
    • pthread_attr_init()初始化线程的属性
    • pthread_attr_setdetachstate()设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
    • pthread_attr_getdetachstate()获取脱离状态的属性
    • pthread_attr_destroy()删除线程的属性
    • pthread_kill()向线程发送一个信号

    问题

    1. 子线程同时执行 ABC 三个同步任务,全部执行完毕后再在自线程执行三个同步任务 EDF,应该怎样做?
      • 可以使用 GCD 的group 或者 NSOperation 的 依赖
      • 可以使用 dispatch_barrier 稍简单一些
    2. 将问题 1 中的 ABC 三个任务改为异步任务如 AFN 网络请求,全部回调成功后进行数据整合,应该怎样做?
      1. 使用信号量
      2. 使用 GCD 的 group 也可以。dispatch_group_enterdispatch_group_leave 搭配使用
    3. 线程的生命周期是怎样的?
      可参考 线程的生命周期以及常驻线程

    参考

    1. iOS 多线程:『NSOperation、NSOperationQueue』详尽总结
    2. iOS基础深入补完计划--多线程(面试题)汇总
    3. 深入浅出 GCD 之 dispatch_group
    4. iOS多线程相关面试题

    相关文章

      网友评论

          本文标题:iOS 多线程

          本文链接:https://www.haomeiwen.com/subject/ynbzlktx.html