iOS底层-多线程(一)

简介

  1. iOS中的常见多线程方案 图4
  2. GCD的常用函数
    1. GCD中有2个用来执行任务的函数
      1. 用同步的方式执行任务

         dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
         queue:队列
         block:任务
        
      2. 用异步的方式执行任务

         dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
        
    2. GCD源码:https://github.com/apple/swift-corelibs-libdispatch
  3. GCD的队列
    1. GCD的队列可以分为2大类型
      1. 并发队列(Concurrent Dispatch Queue)
        1. 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
        2. 并发功能只有在异步(dispatch_async)函数下才有效
      2. 串行队列(Serial Dispatch Queue)
        1. 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
  4. 容易混淆的术语
    1. 有4个术语比较容易混淆:同步、异步、并发、串行
      1. 同步和异步主要影响:能不能开启新的线程
        1. 同步:在当前线程中执行任务,不具备开启新线程的能力
        2. 异步:在新的线程中执行任务,具备开启新线程的能力
      2. 并发和串行主要影响:任务的执行方式
        1. 并发:多个任务并发(同时)执行
        2. 串行:一个任务执行完毕后,再执行下一个任务
  5. 各种队列的执行效果 图4

    1. 注意:使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)!!!
      1. 使用使用sync函数
      2. 当前队列
      3. 当前队列是串行队列
    2. 所谓的死锁就是卡主当前的线程崩溃

       - (void)viewDidLoad {
           [super viewDidLoad];
           NSLog(@"==任务1==");
           //1. 拿到当前的队列-主队列
           dispatch_queue_t queue = dispatch_get_main_queue();
           //2. 将block这个任务放到主队列中
           //队列特点:FIFO,先进先出.
           //dispatch_sync:立马在当前线程执行block,执行完才能往下走
           dispatch_sync(queue, ^{
               NSLog(@"==任务2==");
           });
                  
           NSLog(@"==任务3==");
       }
      
      1. 分析为何死锁:
        1. 队列是存放任务的,而且任务是按一定顺序的
          1. 当前主队列的任务:1.viewDidLoad 2. 任务2
        2. 线程是执行任务的,按照队列中的任务顺序执行任务
          1. 正常情况下:
            • 执行viewDidLoad任务
              • 任务1
              • dispatch_sync函数
              • 任务3
            • 执行“任务2”block
          2. 但是在执行viewDidLoad内部的dispatch_sync函数时,该函数的目的是将“任务2”立刻执行,执行完才能继续执行任务3
          3. 由于任务2是排在viewDidLoad后面的,因此dispatch_sync函数肯定取”任务2”失败,导致函数不能返回,而“任务3”又是在dispatch_sync函数执行完之后才能执行,所以造成了卡死
    3. 如果使用dispatch_async就不会卡死

        /**
        dispatch_async函数不要求立刻在当前线程执行block里面的任务,因此,不会卡线程
        */
       dispatch_async(queue, ^{
            NSLog(@"==任务2==");
       });
      
      1. 打印:

         ==任务1==
         ==任务3==
         ==任务2==
        
    4. 分析死锁的关键:线程执行的任务顺序 是否跟 队列中存放的任务顺序一致,一致则没问题,不一致则卡住

知识点

  1. 下面代码的打印结果是什么?

     -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        
         //在子线程中调用
         dispatch_async(dispatch_get_global_queue(0, 0), ^{
             NSLog(@"1");
             //这个没有问题: 1,2,3
     //        [self performSelector:@selector(test) withObject:self];
             //打印: 1,3
             [self performSelector:@selector(test) withObject:self afterDelay:1];
             NSLog(@"3");
         });
            
         //在主线程中调用
         //1,3,2
     //    NSLog(@"1");
     //    [self performSelector:@selector(test) withObject:self afterDelay:1];
     //    NSLog(@"3");
     }
        
     -(void)test{
         NSLog(@"2");
     }
    
    1. 打印结果为:1,3
    2. performSelector:withObject:afterDelay: 底层分析
      1. 右击进入该方法,发现在NSRunloop.h文件中定义的,因此一定跟runloop有关
      2. 猜测底层:
        1. 底层先定义了一个NStimer定时器
        2. 然后将定时器添加到runloop中
        3. 在主线程中自动开启了主线程runloop,所以能够正常执行
        4. 但是在新开的子线程中,并没有手动(子线程需要手动开启runloop)开启runloop,那么子线程一但代码执行完成就死了,等延迟时间到以后,子线程已死,当然无法执行
        5. 所以无法执行2
    3. 这么就能够调用正常了

       dispatch_async(dispatch_get_global_queue(0, 0), ^{
           NSLog(@"1");
           //打印: 1,3,2
           [self performSelector:@selector(test) withObject:self afterDelay:1];
           NSLog(@"3");
           //子线程需要手动开启runloop
           [[NSRunLoop currentRunLoop] run];
       });
      
  2. 下面代码的打印结果是什么?

     -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
         NSThread *thread = [[NSThread alloc] initWithBlock:^{
             NSLog(@"1");
         }];
         [thread start];
         [self performSelector:@selector(test) onThread:thread withObject:self waitUntilDone:YES];
     }
        
     -(void)test{
         NSLog(@"2");
     }
    
    1. 打印: 1,然后崩溃
    2. 因为start之后,会在子线程执行block,执行完成以后,子线程thread已经销毁,因此再去执行test会崩溃
    3. 可以在block内部添加runloop,让线程thread不死,这么就可以执行test了

       NSThread *thread = [[NSThread alloc] initWithBlock:^{
           NSLog(@"1");
           [[NSRunLoop currentRunLoop] addPort:NSPort.port forMode:NSDefaultRunLoopMode];
           [[NSRunLoop currentRunLoop] run];
       }];
      

GNUstep

  1. 苹果的Foundation框架是不开源的,那么如果我们想看苹果的源码该如何办? 比如NSArray、NSRunloop等
    1. 方法一:通过打断点->debug->debug workFlow->Disassembly ,转化成汇编来分析
    2. 方法二:通过GNUstep查看
  2. GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍
  3. 源码地址:http://www.gnustep.org/resources/downloads.php
  4. 虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值
  5. 下载foundation框架:GNUstep Base,查看performSelector:withObject:afterDelay:方法实现:
    1. 的确是定义了一个定时器,然后将定时器添加到当前线程的runloop中,但是并没有开启这个线程runloop。
  6. 汇编调试小知识,重要:!!!!!!
    1. 断点进入汇编时,如果想要汇编一句一句执行,而且遇到函数调用call ...进入函数,那就用si指令; 如果不进入函数,就用ni指令。

队列组的使用

  1. 背景简介
    1. 平时在进行多线程处理任务时,有时候希望多个任务之间存在着一种联系,希望在所有的任务执行完后做一些总结性处理。
    2. 那么就可以将多个任务放在一个任务组中进行统一管理。dispatch提供了相应的API供我们完成这一需求。
  2. dispatch_group_t相关API介绍

     //1. 将block任务添加到queue队列,并被group组管理
     dispatch_group_async(group, queue, block);
        
     //2. 声明dispatch_group_enter(group)下面的任务由group组管理,group组的任务数+1
     dispatch_group_enter(group);
     //3. 相应的任务执行完成,group组的任务数-1
     dispatch_group_leave(group);
     //4. 创建一个group组
     dispatch_group_create();
        
     //5. 等待tiemr秒的时间,不管队列中任务是否完成,都继续往下执行,如果该时间内任务完成了就返回0,否则是非零值
     dispatch_group_wait(group1, DISPATCH_TIME_FOREVER);
        
     //6. 监听group组中任务的完成状态,当所有的任务都执行完成后,触发block块,执行总结性处理。
     dispatch_group_notify(group1, queue1,block);
    
  3. 常见用法的区别
    1. 在使用group组处理任务时,常见的有两种组合。
      1. 第一种:

         dispatch_group_async(group, queue, block);
         dispatch_group_notify(group1, queue1, block);
        
        1. 在这种组合下,根据任务是同步、异步又分为两种,这两种组合的执行代码与运行结果如下
          1. 同步任务:

             dispatch_queue_t queue1 = dispatch_queue_create("dispatchGroupMethod1.queue1", DISPATCH_QUEUE_CONCURRENT);
             dispatch_group_t group1 = dispatch_group_create();
                                
             dispatch_group_async(group1, queue1, ^{
                 dispatch_sync(queue1, ^{
                     for (NSInteger i =0; i<3; i++) {
                         sleep(1);
                         NSLog(@"%@-同步任务执行-:%ld",@"任务1",(long)i);
                     }
                 });
             });
             dispatch_group_async(group1, queue1, ^{
                 dispatch_sync(queue1, ^{
                     for (NSInteger i =0; i<3; i++) {
                         sleep(1);
                         NSLog(@"%@-同步任务执行-:%ld",@"任务2",(long)i);    
                     }
                 });
             });
                                
             //等待上面的任务全部完成后,会往下继续执行 (会阻塞当前线程)
             //    dispatch_group_wait(group1, DISPATCH_TIME_FOREVER);
                                
             //等待上面的任务全部完成后,会收到通知执行block中的代码 (不会阻塞线程)
             dispatch_group_notify(group1, queue1, ^{
                 NSLog(@"Method1-全部任务执行完成");
             });
            
            1. 同步任务运行结果:

               任务1-同步任务执行-:0
               任务1-同步任务执行-:0
               任务1-同步任务执行-:1
               任务1-同步任务执行-:1
               任务1-同步任务执行-:2
               任务1-同步任务执行-:2
               Method1-全部任务执行完成
              
          2. 异步任务:

            1. 将上面的任务都改成:

               dispatch_async(queue1, ^{
                   for (NSInteger i =0; i<3; i++) {
                       sleep(1);
                       NSLog(@"%@-异步任务执行-:%ld",@"任务1",(long)i);     
                   }
               });
              
              1. 执行结果:

                 Method1-全部任务执行完成
                 任务2-异步任务执行-:0
                 任务1-异步任务执行-:0
                 任务2-异步任务执行-:1
                 任务1-异步任务执行-:1
                 任务2-异步任务执行-:2
                 任务1-异步任务执行-:2
                
              2. 结论:dispatch_group_async(group, queue, block) 和 dispatch_group_notify(group1, queue1, block) 组合在执行同步任务时正常,在执行异步任务时不正常。

      2. 第二种:

         dispatch_group_enter(group);
         dispatch_group_leave(group);
         dispatch_group_notify(group1, queue1,block);
        
        1. 同步任务:

           dispatch_queue_t queue2 = dispatch_queue_create("dispatchGroupMethod2.queue2", DISPATCH_QUEUE_CONCURRENT);
           dispatch_group_t group2 = dispatch_group_create();
              
           dispatch_group_enter(group2);
           dispatch_sync(queue2, ^{
               for (NSInteger i =0; i<3; i++) {
                   sleep(1);
                   NSLog(@"%@-同步任务执行-:%ld",@"任务1",(long)i);
                                  
               }
               dispatch_group_leave(group2);
           });
                  
           dispatch_group_enter(group2);
           dispatch_sync(queue2, ^{
               for (NSInteger i =0; i<3; i++) {
                   sleep(1);
                   NSLog(@"%@-同步任务执行-:%ld",@"任务2",(long)i);
               }
               dispatch_group_leave(group2);
           });
                          
           //等待上面的任务全部完成后,会往下继续执行 (会阻塞当前线程)
           //    dispatch_group_wait(group2, DISPATCH_TIME_FOREVER);
                          
           //等待上面的任务全部完成后,会收到通知执行block中的代码 (不会阻塞线程)
           dispatch_group_notify(group2, queue2, ^{
               NSLog(@"Method2-全部任务执行完成");
           });
          
          1. 打印,没问题,同步任务正常
        2. 异步任务:

          1. 将上面的任务换成:

              dispatch_async(queue2, ^{
                 for (NSInteger i =0; i<3; i++) {
                     sleep(1);
                     NSLog(@"%@-异步任务执行-:%ld",@"任务1",(long)i);
                 }
                 dispatch_group_leave(group2);
             });
            
          2. 打印正常
          3. 结论: dispatch_group_enter(group)、dispatch_group_leave(group) 和 dispatch_group_notify(group1, queue1,block) 组合在执行同步任务时正常,在执行异步任务时正常。

多线程的安全隐患

  1. 资源共享
    1. 1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
    2. 比如多个线程访问同一个对象、同一个变量、同一个文件
  2. 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题
  3. 多线程安全隐患分析 图4
  4. 多线程安全隐患的解决方案 图4

    1. 解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
    2. 常见的线程同步技术是:加锁
    3. 线程一访问数据开始,将数据锁住,不允许其他线程访问,直到当前线程访问完毕,锁打开,其他线程允许访问,而且同样上锁
  5. 代码举例:

     -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
         [self saleTickets];
     }
        
        
     -(void)saleTicket{
         int oldTicketsCount = self.ticketsCount;
         sleep(.2);
         oldTicketsCount--;
         self.ticketsCount = oldTicketsCount;
         NSLog(@"还剩%d张票====%@",oldTicketsCount,[NSThread currentThread]);
     }
        
     -(void)saleTickets{
         self.ticketsCount = 15;
         dispatch_async(dispatch_get_global_queue(0, 0), ^{
             for (int i =0; i<2; i++) {
                 [self saleTicket];
             }
         });
         dispatch_async(dispatch_get_global_queue(0, 0), ^{
             for (int i =0; i<2; i++) {
                 [self saleTicket];
             }
         });
         dispatch_async(dispatch_get_global_queue(0, 0), ^{
             for (int i =0; i<2; i++) {
                 [self saleTicket];
             }
         });
     }
    
    1. 打印:

       还剩12张票====<NSThread: 0x60000129db80>{number = 5, name = (null)}
       还剩13张票====<NSThread: 0x60000129dc40>{number = 4, name = (null)}
       还剩14张票====<NSThread: 0x6000012f4980>{number = 3, name = (null)}
       还剩11张票====<NSThread: 0x60000129dc40>{number = 4, name = (null)}
       还剩11张票====<NSThread: 0x6000012f4980>{number = 3, name = (null)}
       还剩10张票====<NSThread: 0x60000129db80>{number = 5, name = (null)}
      
    2. 可以看到很显然是错误的

什么时候采用用到锁呢?(注意理解!!!)

  1. 多条线程访问一个资源的时候
  2. 这个资源可以理解为一个数组、字典、甚至是一个属性
  3. 如果是简单的读取,不影响,但是如果需要改变,那就必须加锁了
  4. 比如:

     for (int i = 0;i<5;i++) {
        NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
         [thread start];
     }
        
     //n个线程调用同一个方法
     - (void)test {
         int a = 10;
         int b = 5;
         //如果修改需要加锁
         self.age = 10;
            
         //仅仅是访问,就没必要
         NSLog(@"%d",self.age);
     }
    

iOS中的线程同步方案

  1. 主要有以下方案

     OSSpinLock
     os_unfair_lock
     pthread_mutex
     dispatch_semaphore
     dispatch_queue(DISPATCH_QUEUE_SERIAL)
     NSLock
     NSRecursiveLock
     NSCondition
     NSConditionLock
     @synchronized
    

OSSpinLock

  1. OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
  2. 目前已经不再安全,可能会出现优先级反转问题
    1. 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
      1. 线程有优先级的,优先级高的线程,CPU执行的次数多
      2. 假设线程1的优先级比线程2高
      3. 当前是线程2访问资源,然后加锁
      4. 但此时线程1也来访问资源,发现资源已经被加锁,那么线程1就一直等待,由于线程1的优先级比较高,那么可能CPU就一直使用线程1,导致线程2不能正常的访问资源,然后解锁
      5. 从而造成线程1一直占用这CPU资源,线程2不能正常解锁
    2. 需要导入头文件#import <libkern/OSAtomic.h>

       #import "ViewController.h"
       #import <libkern/OSAtomic.h>
       @interface ViewController ()
       @property (nonatomic, assign) int ticketsCount;
       @property (nonatomic, assign) int money;
       @property (nonatomic, assign) OSSpinLock lock;
       @property (nonatomic, assign) OSSpinLock lock1;
       @end
              
       @implementation ViewController
              
       - (void)viewDidLoad {
           [super viewDidLoad];
           self.lock = OS_SPINLOCK_INIT;
           self.lock1 = OS_SPINLOCK_INIT;
       }
              
       -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
       //    [self saleTicketTest];
           [self moneyTest];
       }
              
              
              
       /**
        存取钱演示
        */
       -(void)moneyTest{
           self.money = 100;
           dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
           dispatch_async(queue, ^{
               for (int i =0; i<3; i++) {
                   [self saveMoney];
               }
           });
           dispatch_async(queue, ^{
               for (int i =0; i<3; i++) {
                   [self drawMoney];
               }
           });
       }
              
       /**
        存钱
        */
       -(void)saveMoney{
           //加锁
           OSSpinLockLock(&_lock1);
           int oldMoney = self.money;
           sleep(.2);
           oldMoney+= 50;
           self.money = oldMoney;
           NSLog(@"存50,还剩%d元====%@",oldMoney,[NSThread currentThread]);
           //解锁
           OSSpinLockUnlock(&_lock1);
       }
       /**
        取钱
        */
       -(void)drawMoney{
           //加锁
           OSSpinLockLock(&_lock1);
           int oldMoney = self.money;
           sleep(.2);
           oldMoney-= 20;
           self.money = oldMoney;
            NSLog(@"取20,还剩%d元====%@",oldMoney,[NSThread currentThread]);
           //解锁
           OSSpinLockUnlock(&_lock1);
       }
              
       /**
        卖一张票
        */
       -(void)saleTicket{
           //注意整个过程要用同一把锁,不能每调用一次都重新创建一把锁
           //初始化
       //    OSSpinLock lock = OS_SPINLOCK_INIT;
                  
           //方法1:
           //尝试加锁(如果需要等待就不加锁,直接返回false;如果不需要等待就加锁,返回true)
       //    bool result = OSSpinLockTry(&_lock);
       //      if (result){
       //          int oldTicketsCount = self.ticketsCount;
       //          sleep(.2);
       //          oldTicketsCount--;
       //          self.ticketsCount = oldTicketsCount;
       //          NSLog(@"还剩%d张票====%@",oldTicketsCount,[NSThread currentThread]);
       //          //解锁
       //          OSSpinLockUnlock(&_lock);
       //      };
                  
           //方法二:
           //加锁(先查看当前是否被加锁,没有才加锁。 如果每次都创建一个lock,那么每次锁都是新的,当然没有加,所以每次都会加)
           OSSpinLockLock(&_lock);
                  
           int oldTicketsCount = self.ticketsCount;
           sleep(.2);
           oldTicketsCount--;
           self.ticketsCount = oldTicketsCount;
           NSLog(@"还剩%d张票====%@",oldTicketsCount,[NSThread currentThread]);
           //解锁
           OSSpinLockUnlock(&_lock);
       }
              
       /**
        卖多张票
        */
       -(void)saleTicketTest{
           self.ticketsCount = 15;
           dispatch_async(dispatch_get_global_queue(0, 0), ^{
               for (int i =0; i<2; i++) {
                   [self saleTicket];
               }
           });
           dispatch_async(dispatch_get_global_queue(0, 0), ^{
               for (int i =0; i<2; i++) {
                   [self saleTicket];
               }
           });
           dispatch_async(dispatch_get_global_queue(0, 0), ^{
               for (int i =0; i<2; i++) {
                   [self saleTicket];
               }
           });
       }
      
       @end
      
Table of Contents