深入理解iOS多线程原创
金蝶云社区-honey缘木鱼
honey缘木鱼
6人赞赏了该文章 369次浏览 未经作者许可,禁止转载编辑于2019年04月15日 15:56:58

多线程是每个面试官必选择面试的问题,因为通过多线程可以连接到异步,同步,串行,并行的基本概念,可以根据对多线程的理解程度揣测面试者的水平。自己当初面试时,就遇到这样的问题,回答的模棱两可,自己也不是很理解,这次好好整理一下。



一.基本概念

1.进程-------进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。

2.线程-------1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程,称为主线程)
一个进程(程序)的所有任务都在线程中执行。

进程和线程的对比
1.线程是CPU调用(执行任务)的最小单位。
2.进程是CPU分配资源的最小单位。
3.一个进程中至少要有一个线程。
4.同一个进程内的线程共享进程的资源。

3.多线程------1个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务,多线程技术可以提高程序的执行效率。

4.主线程-------一个iOS程序运行之后,默认会开启一条线程,这个线程也叫做UI线程。

二.实现方案


1.NSThread的使用
(1).创建线程

   // 方法一:创建线程,需要自己开启线程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];

(2).常用方法

//获取当前线程
 NSThread *thread = [NSThread currentThread];//判断当前是否在多线程
 [NSThread isMultiThreaded]//判断当前是否在主线程
 [NSThread isMainThread]//让当前线程睡眠几秒
 [NSThread sleepForTimeInterval:3];  //回到主线程
 [self performSelectorOnMainThread:<#(SEL)#> withObject:<#(id)#> waitUntilDone:<#(BOOL)#>]

(3).NSThread线程的状态



启动线程
- (void)start; 
// 进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态阻塞(暂停)线程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;// 进入阻塞状态强制停止线程
+ (void)exit;// 进入死亡状态

(4).NSThread多线程安全问题
安全原因:1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同一个文件。那么当多个线程访问同一块资源时,其中有的线程改变访问对象,很容易引发数据错乱和数据安全问题。

解决办法:
互斥锁使用格式

@synchronized(锁对象) { 

// 需要锁定的代码 
 }

通过一个售票实例来看一下线程安全的重要性

- (void)viewDidLoad {
    [super viewDidLoad];    // 总票数为30
    self.numTicket = 30;   
     self.thread01 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) 
     object:nil];  
       self.thread01.name = @"售票员01";   
       self.thread02 = [[NSThread alloc]initWithTarget:self 
       selector:@selector(saleTicket) object:nil];   
       self.thread02.name = @"售票员02";   
       self.thread03 = [[NSThread alloc]initWithTarget:self
       selector:@selector(saleTicket) object:nil];   
       self.thread03.name = @"售票员03";   
       // Do any additional setup after loading the view.
  }

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self.thread01 start];
    [self.thread02 start];
    [self.thread03 start];
}// 售票-(void)saleTicket
{    while (1) {       
 // 锁定的时候,其他线程没有办法访问这段代码
           @synchronized (self) {  
                   
         // 模拟售票时间,我们让线程休息0.05s
        [NSThread sleepForTimeInterval:0.05];        
        if (self.numTicket > 0) {           
         self.numTicket -= 1;            
         NSLog(@"%@卖出了一张票,还剩下%zd张票",[NSThread currentThread].name,self.numTicket);
        }else{            
        NSLog(@"票已经卖完了");           
         break;
        }
       }
    }
}

当没有加互斥锁的时候我们看一下输出:


可以看出出现22张票为两次,出现数据混乱的现象。
加了互斥锁之后打印的数据输出:


总结:
互斥锁的使用前提:多条线程抢夺同一块资源时
注意:锁定1份代码只用1把锁,用多把锁是无效的
互斥锁的优缺点
优点:能有效防止因多线程抢夺资源造成的数据安全问题
缺点:需要消耗大量的CPU资源


(5).NSThread线程之间的通信
做线程间通信---------在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信,例如我们在子线程完成下载图片后,回到主线程刷新UI显示图片。

线程间通信的体现
1个线程传递数据给另1个线程。
在1个线程中执行完特定任务后,转到另1个线程继续执行任务。

线程间通信常用的方法

// 返回主线程- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;// 返回指定线程- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;

2.GCD的使用
1.基本概念
GCD---------全称 Grand Central Dispatch,可以称之为大中央调度。实际上GCD是管理着一个线程池,如何创建线程,如何回收线程,以及分配多少个线程,这些都是GCD来控制的。在开发中,程序员是不用操作线程的相关事情,程序员只需要把应该做的操作放到相应的队列里面即可。

GCD中有多种队列,其中自定义的队列有两种:串行队列和并行队列。
串行队列---------队列中的任务只会顺序执行,执行完一个任务后,才会执行下一个任务。

并行队列----------可以一次执行多个任务。

GCD中有两种操作,分别是同步操作和异步操作。

1:同步操作:不会新开线程,任务创建后必须执行完才可以继续走下去。

2:异步操作:会开启新的线程,任务创建后可以先不执行,过会在执行。

2.串行,并行,同步,异步组合特点


(1).并行队列+异步执行(同一时刻可能会同时执行多个任务,开启多个线程,且每个任务的结束时间是不确定的)

 //创建一个并行队列
    dispatch_queue_t queue = dispatch_queue_create("TestGCD", DISPATCH_QUEUE_CONCURRENT);    //使用异步函数封装三个任务
    dispatch_async(queue, ^{       
     NSLog(@"任务1---%@", [NSThread currentThread]);
    });    
    dispatch_async(queue, ^{       
     NSLog(@"任务2---%@", [NSThread currentThread]);
    });   
     dispatch_async(queue, ^{        
         NSLog(@"任务3---%@", [NSThread currentThread]);
    });

打印结果:



(2).串行队列+异步执行(开启新的线程,所有的任务都在新的线程上顺序执行)

  //创建一个串行队列
    dispatch_queue_t queue = dispatch_queue_create("TestGCD", DISPATCH_QUEUE_SERIAL);    //使用异步函数封装三个任务
    dispatch_async(queue, ^{       
     NSLog(@"任务1---%@", [NSThread currentThread]);
    });    
    dispatch_async(queue, ^{       
     NSLog(@"任务2---%@", [NSThread currentThread]);
    });   
     dispatch_async(queue, ^{       
      NSLog(@"任务3---%@", [NSThread currentThread]);
    });

打印结果:



(3).主队列+异步执行(在主线程中顺序执行)

 //获取主队列
    dispatch_queue_t queue = dispatch_get_main_queue();    //使用同步函数封装三个任务
  dispatch_async(queue, ^{     
     NSLog(@"任务1---%@", [NSThread currentThread]);
    });    
     dispatch_async(queue, ^{      
       NSLog(@"任务2---%@", [NSThread currentThread]);
    });   
     dispatch_async(queue, ^{       
      NSLog(@"任务3---%@", [NSThread currentThread]);
    });

打印结果:


(4)并行队列+同步执行(并行队列可以一次开始多个任务,但实际上仍旧是每个任务都在主线程上执行,且按顺序执行)

 //创建一个并行队列
    dispatch_queue_t queue = dispatch_queue_create("TestGCD", DISPATCH_QUEUE_CONCURRENT);    //使用同步函数封装三个任务
    dispatch_sync(queue, ^{      
      NSLog(@"任务1---%@", [NSThread currentThread]);
    });     
    dispatch_sync(queue, ^{       
     NSLog(@"任务2---%@", [NSThread currentThread]);
    });     
    dispatch_sync(queue, ^{     
       NSLog(@"任务3---%@", [NSThread currentThread]);
    });


打印结果:


(5).串行队列+同步执行(不会新建线程,而且任务是一个一个的执行)

//创建一个串行队列
    dispatch_queue_t queue = dispatch_queue_create("TestGCD", DISPATCH_QUEUE_SERIAL);    //使用同步函数封装三个任务
    dispatch_sync(queue, ^{       
     NSLog(@"任务1---%@", [NSThread currentThread]);
    });    
     dispatch_sync(queue, ^{      
       NSLog(@"任务2---%@", [NSThread currentThread]);
    });     
    dispatch_sync(queue, ^{       
     NSLog(@"任务3---%@", [NSThread currentThread]);
    });

打印结果:


(6).主队列+同步执行(死锁)

 //获取主队列
    dispatch_queue_t queue = dispatch_get_main_queue();    //使用同步函数封装三个任务
  dispatch_sync(queue, ^{       
   NSLog(@"任务1---%@", [NSThread currentThread]);
    });     
    dispatch_sync(queue, ^{       
     NSLog(@"任务2---%@", [NSThread currentThread]);
    });    
    dispatch_sync(queue, ^{     
       NSLog(@"任务3---%@", [NSThread currentThread]);
    });

死锁原因:
主队列中本身是有一个任务A的(主任务),且该任务A还没有执行完。在执行任务A的过程中,又插入了新的同步任务1。我们知道,串行队列中,必须先执行完一个任务后,才能继续执行另一个任务。此时的情况时:

若想继续执行任务A,需要先把任务1执行完,若想继续执行任务1,需要先把任务A执行完,因此造成了阻塞。

3.GCD 常用函数

(1).延迟

//参数1 DISPATCH_TIME_NOW 表示 现在   NSEC_PER_SEC 秒   NSEC_PER_MSEC  毫秒  
即是多长时间加入队列去执行block
//参数2  (int64_t)(2 * NSEC_PER_SEC)  表示的是多长时间去执行
//参数3 dispatch_get_main_queue() 想要加入哪个队列去执行  
 可以自定义dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{        NSLog(@"3秒后执行这个方法");
    });

(2).只执行一次

static dispatch_once_t onceToken;   
 dispatch_once(&onceToken, ^{       
  //只执行一次代码  常用单例
    });

(3).创建主线程(串行)

dispatch_async(dispatch_get_main_queue(), ^{     
   //刷新界面代码
    });

(4).创建异步线程(并行)

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{   
     //比较耗时的代码放这里
    });

(5).dispatch_barrier_async的使用(dispatch_barrier_async是在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行)

dispatch_queue_t queue = dispatch_queue_create("create_asy_queue", DISPATCH_QUEUE_CONCURRENT);    dispatch_async(queue, ^{        NSLog(@"dispatch_async1");
    });    dispatch_async(queue, ^{     
       NSLog(@"dispatch_async2");
    });
    dispatch_barrier_async(queue, ^{    
        NSLog(@"dispatch_barrier_async");     
           dispatch_async(dispatch_get_main_queue(), ^{      
                 NSLog(@"刷新界面");
        });
        
    });    dispatch_async(queue, ^{
    
        [NSThread sleepForTimeInterval:1];    
            NSLog(@"dispatch_async3");
    });

打印结果:


4.GCD在项目中的应用


(1).上文中的延迟和单例

(2).在开发中,遇到N个网络请求都完成后更新UI的需求,这时使用dispatch_group_async

 /** 于是可以建立一个分组,让多个任务形成一个组,下面的代码在组中多个任务都执行完毕之后再执行后续的任务: **/
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    /** 创建一个group组 **/
    dispatch_group_t group = dispatch_group_create(); 
       /** 将block任务添加到queue队列,并被group组管理 **/
    dispatch_group_async(group, queue, ^{
        NSLog(@"任务1");
    });
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"任务2");
    });
    
    dispatch_group_notify(group,dispatch_get_main_queue(),^{
        NSLog(@"更新UI");
    });

(3).GCD的使用可以让程序在后台较长久的运行。


在没有使用GCD时,当app被按home键退出后,app仅有最多5秒钟的时候做一些保存或清理资源的工作。但是在使用GCD后,app最多有10分钟的时间在后台长久运行。这个时间可以用来做清理本地缓存,发送统计数据等工作。

 // AppDelegate.h文件
    @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundUpdateTask;    
    // AppDelegate.m文件
    - (void)applicationDidEnterBackground:(UIApplication *)application
    {
        [self beingBackgroundUpdateTask];        // 在这里加上你需要长久运行的代码
        [self endBackgroundUpdateTask];
    }
    
    - (void)beingBackgroundUpdateTask
    {        self.backgroundUpdateTask = [[UIApplication sharedApplication] 
    beginBackgroundTaskWithExpirationHandler:^{
            [self endBackgroundUpdateTask];
        }];
    }
    
    - (void)endBackgroundUpdateTask
    {
        [[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask];        self.backgroundUpdateTask = UIBackgroundTaskInvalid;
    }

3. NSOperation


NSOperation是一个抽象的基类,表示一个独立的计算单元,可以为子类提供有用且线程安全的建立状态,优先级,依赖和取消等操作。系统已经给我们封装了NSBlockOperation和NSInvocationOperation这两个实体类。

1.怎么使用NSOperation

(1).使用NSInvocationOperation

 NSInvocationOperation *operation = [[NSInvocationOperation alloc] 
 initWithTarget:self selector:@selector(task) object:nil];    // 调用start方法执行操作operation操作
    [operation start];

(2).NSBlockOperation

NSBlockOperation * operation = [NSBlockOperation blockOperationWithBlock:^{   
 NSLog(@"task0---%@", [NSThread currentThread]);
}];
[operation start];

打印结果:



根据打印的结果我们会发现,直接调用start方法时,系统并不会开辟一个新的线程去执行任务,任务会在当前线程同步执行。

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{        NSLog(@"task0---%@", [NSThread currentThread]);
    }];
    [operation addExecutionBlock:^{      
      NSLog(@"task1----%@", [NSThread currentThread]);
    }];
    [operation addExecutionBlock:^{        
    NSLog(@"task2----%@", [NSThread currentThread]);
    }];    // 开始必须在添加其他操作之后
    [operation start];

打印结果:


根据打印结果:task0的是执行在主线程中的(因为是在主线程中调用start方法),但task1和task2都是在自己的新线程中执行.也就是说:当NSBlockOperation封装的操作数大于1的时候,就会执行异步操作。

(3).自定义NSOperation

  • 创建一个类继承自NSOperation

  • 重写main方法,自动释放池

  • 初始化该操作的时候直接调用alloc及init即可

  • 取消操作(取消正在执行的操作)

以下载图片为例如何自定义NSOperation:

@interface DTDownloaderOperation : NSOperation
//要下载图片的地址@property (nonatomic, copy) NSString *urlString;
//执行完成后,回调的block@property (nonatomic, copy) void (^finishedBlock)(UIImage *img);
+ (instancetype)downloaderOperationWithURLString:(NSString *)urlString 
finishedBlock:(void(^)(UIImage *img))finishedBlock;
@end
@implementation DTDownloaderOperation
+ (instancetype)downloaderOperationWithURLString:(NSString *)urlString finishedBlock:(void (^)(UIImage *))finishedBlock {
    DTDownloaderOperation *op = [[DTDownloaderOperation alloc] init];
    op.urlString = urlString;
    op.finishedBlock = finishedBlock;   
     return op;
}

- (void)main {   
 @autoreleasepool {      
   //模拟网络延时
        [NSThread sleepForTimeInterval:2.0];        //判断是否被取消  取消正在执行的操作
        if (self.isCancelled) {      
              return;
        }       
         NSLog(@"下载图片 %@ %@",self.urlString,[NSThread currentThread]);   
              //假设图片下载完成
        //回到主线程更新UI
            if (self.finishedBlock) {
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{      
              self.finishedBlock(self.urlString);
        }];
      }
    }
}

-------------------------------------ViewController----------------------------------

//全局队列@property (nonatomic, strong) NSOperationQueue *queue;
/懒加载
- (NSOperationQueue *)queue { 
   if (_queue == nil) {
        _queue = [[NSOperationQueue alloc] init];
        _queue.maxConcurrentOperationCount = 2;//最大并发数,为1 时就变成了串行执行任务
    }    return _queue;
}

- (void)viewDidLoad {
    [super viewDidLoad]; 
   for (int i = 0; i<20; i++) {
        DTDownloaderOperation *op = [DTDownloaderOperation downloaderOperationWithURLString:@"abc.png" finishedBlock:^(UIImage *img) {            //图片下载完成更新UI
            NSLog(@"更新UI %d  %@",i,[NSThread currentThread]);
        }];
        [self.queue addOperation:op];
    }
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {   
 //设置所有操作的canceled属性为yes
    [self.queue cancelAllOperations];   
     NSLog(@"取消");
}

总结:NSOperation的start方法默认是同步执行任务,这样的使用并不多见,只有将NSOperation与NSOperationQueue进行结合,才会发挥出这种多线程技术的最大功效.当NSOperation被添加到NSOperationQueue中后,就会全自动地执行异步操作.

三.多线程优缺点

  • 优点
    1.适当的提高执行效率
    2.提高资源利用率(cpu 内存)

  • 缺点

  1. 开启大量线程,会降低程序性能

  2. 线程越多,CPU在线程之间的调度越大

  3. 程序设计更复杂,线程之间的通信,线程之间的数据共享


虽然我们在实际开发中多线程用到的很少,那是因为系统和第三方都已经封装好了,我们直接拿来使用,作为程序员,普通水平和大神的区别在哪里? 我感觉不在做项目的能力,项目做多了,都有经验了,而在于对实现原理,底层研究的能力,大神深知写每句代码的原因,自己也可以封装出更好的。



本篇独发金蝶云社区

赞 6