iOS多线程整理 (精)

作者: iOS猿_员 | 来源:发表于2018-11-21 12:32 被阅读1次

    知识点梳理

    1.线程进程的区别:

    > 进程:应用程序的实例
    > 线程:任务调度的基本单元
    

    2.队列种类:

    串行队列、并发队列、主队列(有经过特殊处理的串行队列)、全局队列(属于并发队列)

    > 串行队列:队列中的任务按顺序一个一个执行,任务的执行必须有先后顺序
    > 并发队列:具有并发执行队列中任务的能力
    > 主队列:绑定主线程,所有任务都在主线程中执行
    > 全局队列:系统提供的并发队列
    

    串行并行的区别:

    串行:表示在某个时刻只有一个任务在执行
    并行:表示在某个时刻有多个任务在执行
    

    3.并发与并行的区别:

    并发 Concurrency [kən'kʌrənsɪ]:可以同时接受多个任务,使多个任务得到处理的特性

    1.真实的情况。比如:一个程序猿可以揽10个需求同时去做。一个程序猿在做需求期间可抽空学习或接私活。一个工厂可以接10个订单同时去生产。单核CPU可同时处理多个应用程序。

    2.比如并发队列。并发队列能够处理多个任务,使多个任务不用彼此等待同时得到处理。(扩展:并发队列如何实现并发特性?通过开辟多个子线程去处理这多个任务,以此来实现并发特性)

    3.比如单核CPU实现并发(扩展:单核CPU如何实现并发?通过时间片轮转调度)

    并行 parallel [ˈpærəˌlɛl]:某个时刻多个任务能够同时执行的能力

    1.真实的情况。比如:人可以让两只手一起握拳(人可并行握拳),一个打了10个孔的水管可以同时浇10盆花(打孔水管可并行浇花),等等...

    2.比如多核CPU的并行计算,同一时刻CPU的每个核心可以单独执行指令,上面说到的单核CPU是没有这个能力的。

    其他理解:

    1.系统中有多个任务同时存在可称之为“并发”,系统内有多个任务同时执行可称之为“并行”

    2.并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生

    举例:工厂加工糖果

    不具有并发特性的工厂,无并行能力:工厂每次只能接一个订单,多的订单往后排,一个做完再做下一个;只有一台机器生产糖果

    具有并发特性的工厂,无并行能力:工厂可以一次性接多个订单;只有一台机器交替生产这多个订单的糖果

    不具有并发特性的工厂,具有并行能力:工厂每次只能接一个订单,多的订单往后排,一个做完再做下一个;有多台机器一起生产这一个订单的糖果

    具有并发特性的工厂,具有并行能力:工厂可以一次性接多个订单;有多台机器一起生产这多个订单的糖果

    举例:CPU执行任务

    单核cpu非并发执行任务:单核CPU一次处理一个完整任务

    单核cpu并发执行任务:单核CPU交替处理多个任务,每次只处理某个任务的一部分

    多核cpu非并发执行任务:多核CPU一次处理一个任务,将任务拆分成多个子任务,多个核心同时单独的执行这些子任务

    多核cpu并发执行任务:多核CPU一次处理多个任务,将任务拆分,多个核心同时单独的执行这些子任务

    问题:

    既然串行和并行是反义词,为什么都说并发队列,而不说并行队列:计算机硬件和系统可能并非能真正的并行执行任务。比如单核cpu,也可以实现并发,但是不具有并行能力。

    4.操作:

    > 同步:synchronize[ˈsɪŋkrənəs],同步任务需要使当前任务等待
    > 异步:asynchronous[e'sɪŋkrənəs],异步任务无需使当前任务等待
    

    同步异步的理解:

    我们写的的代码其实是被包裹在一个任务中的,这个任务在队列中排队,然后轮到它时就在队列绑定的线程中执行。

    如下,整块代码也都是被包裹在一个任务中,这个任务在主队列排队,然后最后轮到它时放到主线程执行。

    // 任务1最开始....
    ...
    // 以下的代码也只是任务1中的一个片段
    // 主线程环境中
    dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
        // 此处大括号包裹的整个是任务2
        // do something in task2
    });
    ...
    // 任务1最末尾...
    

    等同于:

    // 任务1最开始....
    ...
    // 以下的代码也只是任务中的一个片段
    // 主线程环境中
    // do something in task1
    ...
    // 任务1最末尾...
    

    等价设定:

    “当前线程执行的任务” <=> “任务1”
    “需要执行的任务”:<=> “任务2”
    

    1.同步:任务2与任务1同步,任务1要等待任务2执行完毕后才能继续执行(扩展猜测:由于是同步,任务1要等待任务2,所以此时开新线程执行任务2和不开新线程执行任务2,从期望的结果来看没什么区别,则直接在当前线程中执行任务2即可)

    2.异步:任务2与任务1异步,任务1不用等待任务2完成就可继续执行

    总结:同步异步是针对多线程代码和当前所在环境之间的关系,用来控制“当前线程执行的任务”是否要等待“需要执行的任务”,与队列无关,与队列中的其他任务无关。

    5.iOS中的多线程规则

    情况 是否新开线程 与当前执行代码所属任务的关系
    串行队列同步 当前线程执行 当前任务需等待
    串行队列异步 新开线程执行(每个任务都在同一个线程执行) 当前任务无需等待
    并发队列同步 当前线程执行 当前任务需等待
    并发队列异步 新开线程执行 当前任务无需等待

    6.扩展知识:执行栈

    1.常被用于存放子程序的返回地址

    2.在调用任何子程序时,主程序都必须暂存子程序运行完毕后应该返回到的地址

    3.如果被调用的子程序还要调用其他的子程序,其自身的返回地址就必须存入执行栈,在其自身运行完毕后再行取回

    4.在递归程序中,每一层次递归都必须在执行栈上增加一条地址,因此如果程序出现无限递归(或仅仅是过多的递归层次),执行栈就会产生栈溢出。

    比如:

    void main() {
        int i = 0
        aMethod()
        bMethod()
    }
    void aMethod {
    }
    void bMethod {
        cMethod()
    }
    void cMethod {
    }
    

    堆栈过程:

    null
    main
    main - aMethod
    main
    main - bMethod
    main - bMethod - cMethod
    main - bMethod
    main
    null
    

    一些问题的理解

    问题一:主线程环境中,在主队列上执行同步任务,为什么会死锁

    1.假设:假设当前执行的代码是包含在任务1中,在主队列上执行的同步任务为任务2。

    // 任务1
    // 主线程环境中
    dispatch_sync(dispatch_get_main_queue(), ^{
        // 任务2
        // do something in task2
    });
    

    2.同步角度思考:由于是是同步任务,所以任务1此时需要等待任务2执行,任务2执行完毕后任务1才能继续执行下去。

    3.队列角度思考:任务2会被加到主队列的队尾,由于串行队列的特性,任务必须一个一个执行。因此任务2需要等待队列中其他任务(包括任务1)都执行完之后才会轮到它去执行。

    4.结果:所以出现了任务2等待任务1,任务1等待任务2的情况,导致死锁。
    此外如果串行队列绑定线程a,那么在线程a环境中,在该串行队列上执行同步任务,也会导致死锁。原因同上。

    问题二:主线程环境中,为什么在新创建的串行队列中执行同步任务就不会死锁

    1.假设:假设当前执行的代码是包含在任务1中,在串行队列上执行的同步任务为任务2。

    // 任务1
    // 主线程环境中
    dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
        // 任务2
        // do something in task2
    });
    

    2.同步角度思考:由于是是同步任务,所以任务1此时需要等待任务2执行,任务2执行完毕后任务1才能继续执行下去。

    3.队列角度思考:任务2会被加到串行队列zcp的队尾,任务2只跟队列zcp中的其他任务有先后顺序关系,跟其他队列上的任务无关,也就是说任务2跟主队列中的其他任务无关,所以任务2不会等待任务1

    4.结果:任务1等待任务2,任务2不用等待任务1,任务2执行完毕后,然后继续执行任务1。

    API介绍

    详细内容可参考:iOS多线程-归纳与总结

    1.NSThread

    管理多线程困难,推荐使用NSOperation和GCD。

    应用场景:

    > 1.使用[NSThread currentThread]获取当前线程
    > 2.使用[NSThread mainThread]获取主线程
    

    2.NSOperation

    GCD的封装,代码风格更OC。

    特点:

    1.可以控制暂停、恢复、停止。suspended、cancel、cancelAllOperations

    2.可以控制任务的优先级。threadPriority和queuePriority

    3.可以设置依赖关系。addDependency和removeDependency

    4.可以控制并发个数。maxConcurrentOperationCount

    5.NSOperation有两个封装的便利子类NSBlockOperation、NSInvocationOperation,他们都使用了并发队列

    队列的种类:

    主队列 [NSOperationQueue mainQueue],是串行队列

    非主队列 [NSOperationQueue new],是并发队列

    NSOperation的执行过程:

    当operation加入到queue中时,会在相关线程中执行operation的start方法,main方法在start方法中调用。

    线程判定:

    根据queue来决定在哪个线程中执行start方法。

    [NSOperationQueue mainQueue]:在主线程中执行

    [NSOperationQueue currentQueue]:在当前线程中执行

    [NSOperationQueue new]:新开线程执行,该队列为并发队列

    start方法和main方法的执行顺序:

    start方法内部做了一些有关安全的逻辑判断,判断结束后执行main。

    因此如果自己写了一个类继承自NSOperation,重写start方法时要注意,main方法会在[super start]中执行,如果不调用[super start]则main方法不执行,另外要注意[super start]与前后代码的执行顺序。

    应用场景:

    可以参考AFNetworking2.x版本中的AFURLConnectionOperation类和AFHTTPRequestOperation

    3.GCD

    1.队列与操作

    队列与操作

    // 串行队列
    dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_SERIAL)
    // 并发队列
    dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_CONCURRENT)
    // 主队列
    dispatch_queue_t queue = dispatch_get_main_queue()
    // 全局队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
    
    // 同步操作
    dispatch_sync(queue, ^{
    })
    // 异步操作
    dispatch_async(queue, ^{
    })
    

    其他功能

    // 暂停队列
    dispatch_suspend(dispatch_object_t object);
    // 恢复队列
    dispatch_resume(dispatch_object_t object);
    

    2.其他内容

    dispatch_after

    // 延迟5秒执行
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    });
    

    dispatch_once

    确保程序执行过程中只被执行一次,且线程安全,常用于单例。

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    });
    

    任务组

    用来处理多个任务都完成后再执行的动作

    // 队列,可以根据情况使用合适的queue
    dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_CONCURRENT);
    // 创建任务组
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        // 任务1
    });
    dispatch_group_async(group, queue, ^{
        // 任务2
    });
    dispatch_group_async(group, queue, ^{
        // 任务3
    });
    dispatch_group_notify(group, queue, ^{
        // 任务1、任务2、任务3都执行完毕之后才会执行这里
    });
    
    // 系统管理队列组:
    dispatch_group_async(group, queue, ^{
        // do something
    });
    // 等价于
    // 手动管理队列组:
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        // do something
        dispatch_group_leave(group);
    });
    
    

    dispatch_semaphore

    // 创建信号量
    dispatch_semaphore_create
    // 信号量-1
    dispatch_semaphore_wait
    // 信号量+1
    dispatch_semaphore_signal
    
    // YYCache中的YYDiskCache类中
    #define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
    #define Unlock() dispatch_semaphore_signal(self->_lock)
    

    dispatch_barrier

    dispatch_barrier_async:
    dispatch_async(queue, block1_for_reading)  
    dispatch_async(queue, block2_for_reading)
    
    dispatch_barrier_async(queue, block_for_writing)
    
    dispatch_async(queue, block3_for_reading)  
    dispatch_async(queue, block4_for_reading)  
    
    /*
    dispatch_barrier_async会把并行队列的运行周期分为这三个过程:
    
    首先等目前追加到并行队列中所有任务都执行完成
    开始执行dispatch_barrier_async中的任务,这时候即使向并行队列提交任务,也不会执行
    dispatch_barrier_async中的任务执行完成后,并行队列恢复正常。
    
    这样一来,使用并行队列和dispatc_barrier_async方法,就可以高效的进行数据和文件读写了。
    */
    

    4.其他问题

    多线程与runloop的关系:

    每个线程都有一个runloop,主线程默认开启,子线程默认休眠。
    一般来讲,一个线程一次只能执行一个任务,执行完毕后线程就会退出,开启runloop可以让线程能随时处理事件但并不退出

    多线程不安全的情况:

    __block int a = 0;
    for (int i = 0; i < 100; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"%d", a++);
        });
    }
    
    // 结果并不为100
    

    原因:

    a++这句代码其实相当于a = a+1,由于代码是在并发队列中异步执行的,所以相当于有100个a = a+1同时执行。

    a++代码的执行过程如下:

    > 1.取到a的值
    > 2.计算a+1的值
    > 3.将计算结果赋值给a
    
    

    正常情况:

    第1次:执行a++,取到a为0,计算a+1结果为1,将1赋值给a。
    

    异常情况:

    第1次:执行a++,取到a为0,
    第10次:执行a++,取到a为0,计算a+1结果为1,将1赋值给a
    第56次:执行a++,取到a为1,计算a+1结果为2,将2赋值给a
    第27次:执行a++,取到a为2,计算a+1结果为3,将3赋值给a
    第1次:计算a+1结果为1,将1赋值给a
    

    以上是一种异常情况的假设,实际的执行情况会更复杂。在第一次取到a为0时,其他线程已经跑了很多句a++的代码使a变成了3,这个时候才开始计算第一次的a+1,a又变成了1。导致前面几次的计算都没意义了。
    引用时间片轮转调度中的一段话:

    在自己的程序运行时不是独一无二的,我们看似很顺畅的工作,其实是由一个个的执行片段构成的,我们眼中相邻的两条语句甚至同一个语句中两个不同的运算符之间,都有可能插入其他线程或进程的动作。


    使用案例

    1.处理耗时任务

    本地持久化:如果在主线程中存储数据,数据量比较大时会阻塞主线程造成页面卡顿。需要新开线程在后台处理。另外还有使用dispatch_barrier_async和CoreData的案例。

    耗时代码处理:如果使用多次数的循环语句,或者是使用非常耗时的api时,会影响到主线程导致卡顿。可以新开线程在后台处理,然后如果有需要刷新UI则在主线程中同步。

    2.网络请求等待

    接口请求:接口请求受网络环境影响,是不可能在主线程请求并等待的。需要新开线程异步请求。如使用NSURLSession的dataTaskWithURL:方法(或NSURLConnection的sendAsynchronousRequest:方法)异步请求;如AFNetworking中异步请求代码。

    加载网络资源:加载网络中的大图或下载文件会很耗时,需要在后台线程加载。如在子线程中使用NSData的dataWithContentsOfURL:下载文件;如SDWebImage的异步下载。

    3.其他情况

    任务组:常会遇到某个逻辑判断需要两个接口中的数据,比如当获取业务线和列表数据之后才渲染页面。就可以用任务组。

    延迟调用:比如一些动画的实现需要延时。

    总结:

    以上就是这篇文章的全部内容了,希望本文的内容对大家具有一定的参考学习价值,同时欢迎大家进入小编iOS交流群:624212887,一起交流学习,谢谢大家的支持

    相关文章

      网友评论

        本文标题:iOS多线程整理 (精)

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