阅读 141

iOS 多线程技术总结

概览

  • 进程与线程的概念
  • 多线程的由来
  • 并行与并发
  • 多线程的实现
  • 串行与并行
  • 线程的几种状态
  • 串行队列与并发队列区别
  • iOS 实现多线程的几种方法(NSOperation/GCD
  • GCD线程阻塞的几种情况
  • NSOperationGCD 的关系以及 NSOperation 的使用、实现
  • iOS 中是怎么定义多线程的?或者说什么情况下是多线程的?
  • pThread、NSThread、GCD、NSOperation 之间的关系
  • 串行队列与主队列的区别
  • 全局队列与并发队列的区别
  • 调度组的作用以及使用

进程与线程

进程是运行中的程序,线程是进程的子集,具有单个处理任务的能力,进程通过对线程的包装,通过时间轮转算法间接实现了程序处理任务的并发,进而多线程技术应运而生。

由于计算机程序处理任务的是按照串行处理的方式进行,如果程序以线程为基准去处理一些任务,假如处理的任务比较耗费时间,例如对与 IO 的操作,那么由于串行处理机制,在时间轮转分发到某个进程的时候,恰巧该进程假如正在处理 IO 流耗时操作的话,如果没有线程的存在,那么该进程就会始终处于处理 IO 操作的过程中,程序就会因此而假死,此时都在等待 IO 操作,而 cpu 此时由于没有其他任务处理,所以也是闲置状态,导致整体cpu效率变低。为了保证这些耗时操作能够独立去某个地方去执行而同时又不影响其他任务的正常执行,提出了多线程的处理方案,让程序运行的时候拆分成多个任务,这些任务分别去分发到不同的地方去单独处理,这些单独的地方就是线程,不同线程包装不同的任务,这样就解决了因为耗时操作而导致的cpu闲置,运行效率变低的问题。

多线程技术的实现

多线程是通过时间轮转算法实现的并发

并发与并行的区别

并行是并发的子集,并行从硬件层面上实现了多线程,所谓的硬件就是一些厂商经常宣传的工艺,几核几线程工艺,从硬件上直接实现了并行。并发实现的另一种方式就是多线程技术,是从软件层面上实现的并发。

NSOperation 与 GCD 的关系以及 NSOperation 的使用、实现

首先, NSOperation是一个抽象类,本身不能通过实例去实现它。其次,NSOperation 是对 GCD 的封装,在原有基础上又添加了一些线程的操作(GCD 中没有的 api), 例如取消线程任务判断线程的执行状态以及控制线程的数量等,这些都是 GCD 不对外暴露的,所以一些三方的框架例如 AFNetworkingSDWebImage 等框架都是使用的 NSOperation 进行的封装。如果想精确的对线程进行操作的话,NSOperation 更适合去进行对应的相关操作。

NSOperation 的实例是通过两个子类进行实现的,分别为 NSBlockOperation 、NSInvocationOperation。两者的区别是 NSBlockOperation 是通过 Block 形式添加任务、而 NSInvocationOperation 是通过方法的形式去添加任务的。除此之外,NSBlockOperation start 之后是并发执行添加的任务的。而 NSInvocationOperation 是非并发执行任务的。 一般情况下,我们需要将创建的线程放在 NSOperationQueue 中去自动执行任务,如果不放在 NSOpertionQueue 中,直接通过手动的 Start 方法去执行的话,那么默认的执行操作是在当前线程中执行任务的,如果执行任务,需要当前的线程处于 ready 状态,如果不在改状态去执行任务的话,系统会抛出异常。如果想在子线程中去执行操作,需要手动去将其放在子线程中执行。也可以通过子类继承 NSOperation 重写 main 方法来实现子线程操作。这样的操作都很麻烦,而且很容易出错,除非特别需要,我们一般都使用 NSOperationQueue 进行NSOperation 的管理。

NSOperationQueue 中添加NSOperation 的时候,默认会在子线程自动执行任务(直接执行或者间接通过 GCD 执行)

pThread、NSThread、GCD、NSOperation之间的关系可以用图形的形式表示如下

关系图

iOS 中是怎么定义多线程的?或者说什么情况下是多线程的?

当有任意的线程从主线程分离出去的时候,App被认为是多线程的,可以通过 isMultiThread 来判断当前 App是否是多线程状态。只要某个子线程被创建后(不是 NSThread 对象),不需要正在运行,就认为是多线程状态。

并发队列与串行队列的区别

并发队列是派发到队列中的任务并发执行,而串行队列是指派发到队列中的任务顺序执行,派发到队列中的任务执行完成之后才能够继续执行派发的下一个任务。

并发队列可以同时执行多个任务,多个任务的执行受限于当前的派发方式(同步派发与异步派发)。

并发队列同步派发会由于同步原因(阻塞当前线程)会执行同步任务直到其完成才能够执行下一条分发的任务。每次只能够执行单一的任务(同步造成当前队列中只有一个任务存在,尽管是并发队列,每次也只能够执行一个任务)。

而并发队列异步派发是同时派发了多个任务,而派发的任务因为是异步派发的,所以同时执行的任务是多个(队列中异步派发了多个任务,所以并行队列能够同时执行多个任务,此时需要新开多个线程处理任务)。

注意: 串行队列,每次只执行一个任务,直到当前的任务执行完成之后,再去执行下一个任务。 并发队列,每次执行多个任务,所以多个任务的执行完成顺序不能够确定(通过开启多个线程实现的)。

任务派发与不同队列执行的情况

下面的表格总结的很详细

几种派发的情况

它们的区别可以总结如下:

  • 同步派发:当前任务不执行完成,不会执行下一条任务
  • 异步派发:当前任务执行过程中,同样可以执行下一条任务
  • 串行队列:必须等待第一个任务执行完成以后,再去调度另一个任务
  • 并发队列:同时调度多个任务,至于这些任务通过几个线程去执行是由 GCD 管理
  • 主队列:全局串行队列,由主线程串行调度,并且有且只有一个
  • 全局队列:没有名称的并发队列

结合例子:

串行队列同步任务

/**串行队列 同步任务
 //串行队列同步分发任务,阻塞当前的线程,不需要开启新的线程去执行
 //受限于串行队列与同步分发的特点,同步分发的任务如果当前的任务不执行完那么就不会去执行下一条任务
 //串行队列则是每次执行分发一条的任务,当前任务完成之后才能够继续执行下一条任务
 */
- (void)serialSyncTask{
    
    dispatch_queue_t queue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
    
    for (int i = 0; i<4; i++) {
        
        dispatch_sync(queue, ^{
           NSLog(@"串行队列同步分发任务-Task(%d),currentThead:%@",i,[NSThread currentThread]);
        });
        
        NSLog(@"分发 Task(%d) 完成",i);
    }
    
    
}

复制代码

串行队列同步任务

串行队列异步任务

/**串行队列 异步任务
 //串行队列异步分发任务,不阻塞当前的线程,所以队列中同一时间分发了三个任务,按照先进先出原则,
 //任务按顺序开始,由于是串行队列,在执行某个任务的时候,是不会去调度执行其他任务的,所以此时依次
 //按照队列中的任务执行完成操作。
 //主队列异步分发任务,不阻塞队列中其他任务的执行
 */
- (void)serialAsyncTask{
    
    /******************************串行队列异步分发任务*********************************************/
    dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
    for(int i = 0; i<4; i++){
        
        dispatch_async(serialQueue, ^{
            sleep(i+1);
            NSLog(@"串行队列异步分发任务-Task(%d),currentThead:%@",i,[NSThread currentThread]);
        });
        
        NSLog(@"分发 Task(%d) 完成",i);
    }
    
    NSLog(@"执行结束");
    
}

复制代码

运行结果如下:

串行队列异步任务

并发队列同步任务

/**
  并发队列 同步任务
 //阻塞当前线程
 //并行队列同一时将任务分发到队列中,同步执行需要当前的任务完成之后才能继续执行其他的任务,是按照顺序进行的,所以此时不会新建线程去处理任务
 */
- (void)concurrentSynac{
    
    dispatch_queue_t queue = dispatch_queue_create("concorrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i<4; i++) {
        
        dispatch_sync(queue, ^{
            sleep(1);
            NSLog(@"并发队列同步分发任务-Task(%d),currentThead:%@",i,[NSThread currentThread]);
        });
        NSLog(@"分发 Task(%d) 完成",i);
        
    }
    
}

复制代码

并发队列同步任务

并发队列 异步任务

/**
 并发队列 异步任务
 不阻塞当前线程
 由于队列中异步添加了多个任务,并发队列同一时间能够执行多个任务,所以需要新建多个线程去处理队列中的任务
 */
- (void)concurrentAsynac{
    
    dispatch_queue_t queue = dispatch_queue_create("concorrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i<4; i++) {
        
        dispatch_async(queue, ^{
            sleep(1);
            NSLog(@"并发队列异步分发任务-Task(%d),currentThead:%@",i,[NSThread currentThread]);
        });
        NSLog(@"分发 Task(%d) 完成",i);
        
    }
    
    
}

复制代码

并发队列 异步任务

死锁的发生

死锁

如果当前队列中 task0正在执行操作,此时如果同步分发任务task1,同步分发阻塞当前线程,从而会执行当前的任务直到当前分配的任务结束,但是,因为是串行队列,串行队列中每次只能执行一个任务,由于 task0没有执行完成,所以 task1需要等待 task0执行完成才能够执行。由于task1要想执行需要等到task0执行完成,而 task0中由于同步分发了task1,需要等到 task1执行完成才能够继续完成当前的任务,两者相互等待对方任务执行完成,最终造成死锁。

相同的情况,主队列同步任务也会造成死锁,具体原因也很类似。

原因:主队列是全局串行队列,如果同步执行任务的话,由于主队列当前的任务没有完成此时是不会调度当前分发的任务的,而此时分发的任务又是同步的,同步分发的任务有个特点就是阻塞当前线程,执行当前的任务知道结束。所以由于上一个任务始终由于分发导致其完成不了,分发的任务又一直在等待其完成,两者造成了一个死循环,不断在等待对方完成,却永远都完成不了,导致死锁的发生。

简洁的说:

  • 主队列:如果主线程正在执行代码,就不调度任务
  • 同步任务:如果队列中前一个任务没有执行完成,就继续等待上一个任务完成再去执行下一个任务
  • 两者相互等待造成死锁

全局队列与并发队列的区别

  • 全局队列是没有队列名字的,并发队列是有名字的。而有名字的队列是可以跟踪到的
  • 一般使用全局队列
  • MRC 中需要手动管理内存,并发队列是通过 creat 出来的,在 MRC 中见到creat就需要 release 操作,而全局队列不需要 release 操作,全局队列有且只有一个

同步任务的用途

一般一些耗时操作会放在一个线程中执行,而当前的这个操作可能需要一些『依赖』关系的操作之后才能够执行,这时候就需要同步的操作了。比如一个界面的展示需要两个接口数据的返回才能够正常展示,那么此时需要同步两个接口的数据,然后最后才去展示数据内容,对于前两个接口的请求数据我们需要将其操作通过同步分发的操作保证每一步执行完成之后在进行下一步操作。如下代码:

- (void)syncThreadUse{
    
    //全局并发队列 异步调度派发
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    

        //并发队列,同步派发
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            
            sleep(3);
            NSLog(@"请求接口一,currentThread:%@",[NSThread currentThread]);
        });
        
        //并发队列,同步派发
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            sleep(1);
            NSLog(@"请求接口二,currentThread:%@",[NSThread currentThread]);
        });
        
        //并发队列,异步派发
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
           
            NSLog(@"异步派发,currentThread:%@",[NSThread currentThread]);
            /*
             注意:
             此时用dispatch_sync 与 dispatch_async效果一样
             dispatch_sync并不造成死锁,因为造成死锁的原因是队列中分发的任务与当前队列中执行的任务相互等待造成的死锁
             此时任务的分发由于不在dispatch_get_main_queue()主线程队列中,所以并没有让分发的任务等待主线程正在执行
             任务结束的情况存在,依据主线程队列的执行任务的特点在主线程已经执行完成任务之后才会去执行当前的任务,来执行
             当前任务
             
             dispatch_sync(dispatch_get_main_queue(), ^{
             NSLog(@"展示界面");
             });
             
             */
            
            //如果对死锁不清楚的话,建议使用dispatch_async方式进行任务的派发,从而避免死锁的发生
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"展示界面");
            });
            
           
        });
        
    });
}

复制代码

结果如下:

image.png

调度组的作用以及使用

Dispatch Group 调度组

  • Dispatch Group 在添加到组里面所有的任务完成的时候发出通知,这些任务可以是同步的,也可以是异步的,哪怕是不同的队列。
  • Dispatch Group只有异步执行
  • 创建的 Dispatch Group,像是一个未完成的计数器
  • dispatch_group_enter 手动通知 Dispatch Group 任务已经开始。它与 dispatch_group_leave 成对出现
  • Dispatch Group 可以包含不同的队列类型,包括:自定义串行队列、主队列(全局串行队列)、并发队列

Dispatch Group 的几个函数

  • dispatch_group_wait 会阻塞当前的线程,知道组里面所有的任务都完成或者等到某个超时完成。 如果所在的任务完成前超时了,该函数会返回一个非零值。可以对此返回值做条件判断来确定是否超出了等待周期。 DISPATCH_TIME_FOREVER 让这个组永远等待。
  • dispatch_apply 类似于 for 循环,能够并发的执行不同的迭代。 这个函数是同步执行的,只会在所有工作完成之后才能返回。 对于并发循环使用 dispatch_apply 可以帮助我们追踪任务的进度
  • dispatch_group_notify 异步方式进行 不会阻塞任何线程 有时候需要在多个异步任务都执行完成之后后续做一些操作

调度组的使用:

- (void)dispatchGroup{
    
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        sleep(6);
        NSLog(@"请求接口1");
    });
    
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        
        NSLog(@"请求接口2");
    });
    
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"请求接口3");
    });
    
    
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"请求接口全部完成");
    });
      
    
}

复制代码

调度组的使用

参考文章: iOS中的多线程技术 多线程之GCD

关注下面的标签,发现更多相似文章
评论