美文网首页
09 - OC多线程之队列任务以及死锁的认识

09 - OC多线程之队列任务以及死锁的认识

作者: iOS之文一 | 来源:发表于2021-11-03 20:18 被阅读0次

    OC底层原理探索文档汇总

    主要内容:

    1、队列的认识
    2、任务的认识
    3、队列和任务搭配使用的面试题
    4、死锁的认识和解决

    队列

    队列就是管理待执行任务的等待队列,用来调度任务给线程执行,符合先进先出原则

    并发和串行决定了任务执行的顺序,并发是多个任务并发执行,串行是指任务顺序执行,也就是一个任务执行完成再执行下一个任务

    • 队列负责调度任务,提交任务给线程执行
    • 队列遵循先进先出原则,在这里指的是先进先调度。而不是先调度完成。
    • 队列底层会维护一个线程池来处理用户提交的任务,线程池的作用就是执行队列管理的任务。
    • 串行队列底层只维护了一个线程,并发队列的底层维护了多个线程
    • 串行队列一次只能处理一个任务,上一个任务处理完成才能处理下一个任务
    • 并发队列可以同时处理多个任务,虽然处理顺序还是先进先出的原则,但是有的任务处理时间比较长,有可能先进后调度完成。
    • 队列是先进先调度,如果是串行队列是先进先调度结束,并发队列并不是先进先调度结束,可以同时调度多个任务

    创建队列:

    - (void)createQueueTest{
        //创建队列
        dispatch_queue_t queue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t queue2 = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t queue3 = dispatch_queue_create("串行队列", NULL);
        //获取已有队列
        //获取主队列
        dispatch_queue_t queue4 = dispatch_get_main_queue();
        //获取全局并发队列
        /*
         第一个参数是优先级,第二个无意义,填0
         */
        dispatch_queue_t queue5 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    }
    

    任务

    任务就是线程要执行的那段代码
    执行任务有两种方式,同步和异步,同步和可以异步决定是否要开启新的线程,同步不开启,异步开启。

    同步函数(sync):

    • 同步函数用来实现线程同步执行任务
    • 只能在当前线程中执行任务,不具备开启新线程的能力
    • 同步函数只能等block执行完成后才能返回

    异步函数(async):

    • 异步函数用来实现线程异步执行任务
    • 异步函数不需要等block执行完成后就可以返回
    • 可以开启新线程

    提交任务的函数很多都有两个版本,一个是传两个参数,传递作为任务的block代码,一个是传三个参数,传递作为任务的方法。

    - (void)createTaskTest{
        /*
         参数一:队列
         参数二:作为任务的block代码
         */
         //在调度队列上提交异步执行的块并立即返回
        dispatch_async(dispatch_queue_t  _Nonnull queue, <#^(void)block#>);
        /*
         参数一:队列
         参数二:上下文
         参数三:作为任务的函数
         */
        dispatch_async_f(<#dispatch_queue_t  _Nonnull queue#>, <#void * _Nullable context#>, <#dispatch_function_t  _Nonnull work#>);
        
        /*
         参数一:队列
         参数二:作为任务的block代码
         */
         //提交块对象以执行,并在该块完成执行后返回。 
        dispatch_sync(dispatch_queue_t  _Nonnull queue, <#^(void)block#>);
        /*
         参数一:队列
         参数二:上下文
         参数三:作为任务的函数
         */
        dispatch_sync_f(<#dispatch_queue_t  _Nonnull queue#>, <#void * _Nullable context#>, <#dispatch_function_t  _Nonnull work#>);
    }
    

    注意:

    • 任务是线程执行的,不是队列执行,队列只是用来调度任务的
    • 任务的执行方式有同步和异步,同步不开辟新线程,异步会开辟新线程

    主队列

    主队列是一种特殊的串行队列,特殊在于只使用在主线程和主Runloop中,并且是在启动APP时自动创建的。
    主队列是指主线程的队列,是一个串行队列,在主线程去执行其实就是放入了主队列

    全局并发队列

    全局并发队列是系统提供的,开发者可以直接使用的一个并发队列,没有其他的特殊之处。

    在获取时可以给定优先级

    • DISPATCH_QUEUE_PRIORITY_HIGH: QOS_CLASS_USER_INITIATED
    • DISPATCH_QUEUE_PRIORITY_DEFAULT: QOS_CLASS_DEFAULT
    • DISPATCH_QUEUE_PRIORITY_LOW: QOS_CLASS_UTILITY
    • DISPATCH_QUEUE_PRIORITY_BACKGROUND: QOS_CLASS_BACKGROUND

    总结

    任务队列线程的关系.png
    • 线程有两种方式执行任务,同步和异步,分别通过同步函数和异步函数来实现
    • 同步函数添加的任务,该线程执行完这个任务才可以执行其他任务,异步函数添加的任务,线程可以并发执行该任务
    • 任务存放在队列中,队列有两种调度方式,串行和并发,串行只能顺序处理任务,并发可以同时处理多个任务
    • 线程想要并发执行任务,需要异步函数添加任务给并发队列
    • 如果队列是串行的,即使线程可以并发执行任务也不行,因为队列是串行调度给线程的。
    • 同步函数需要等待block执行完成才可以返回
    • 异步函数不需要等待block执行完成就可以返回

    队列和任务的搭配使用

    任务和队列以及线程的执行关系.png

    说明:

    • 同步不开启新线程,异步会开启新线程
    • 并发队列可以并发调度任务,串行队列只能顺序调度任务
    • 只有并发队列提交给线程异步执行的任务才可以异步执行

    总结:

    执行类型.png

    队列和任务的常见类型代码演示

    1、基础写法

    给一个串行队列添加了一个任务,该任务需要异步执行

    - (void)syncTest{
        // 把任务添加到队列 --> 函数
        // 任务 _t ref c对象
        dispatch_block_t block = ^{
            NSLog(@"hello GCD");
        };
        //串行队列
        dispatch_queue_t queue = dispatch_queue_create("wy", NULL);
        // 函数
        dispatch_async(queue, block);
        
    }
    

    2、主队列同步

    - (void)mainSyncTest{
        
        NSLog(@"0");
        // 等
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"1");
        });
        NSLog(@"2");
    }
    

    执行结果: 0,之后死锁

    死锁.png

    说明:

    • 在主队列中添加了三个任务,任务0,同步任务块,任务2
    • 同步任务块的任务本身就是给主队列添加了任务1
    • 因此现在主队列的任务有四个,并且顺序为任务0、同步任务块、任务2、任务1,这几个任务依次执行
    • 同步任务块需要等待任务1的完成才能返回,但是任务1的完成又在同步任务块的后面
    • 所以造成了同步任务块和任务1的相互等待,死锁导致程序崩溃

    注意:造成死锁是同步任务块和任务1的相互等待,与任务2没关系,就算删掉任务2,也会死锁

    3、 主队列异步

    - (void)mainAsyncTest{
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"1");
        });
        NSLog(@"2");
    }
    

    执行结果: 21
    说明:

    • 主队列中添加两个任务,异步任务块和任务2
    • 异步任务块的任务本身是给主队列添加了一个任务1
    • 这样主队列有三个任务,并且顺序为异步任务块、任务2、任务1
    • 因为任务1是异步提交的,也就是线程以异步的方式来执行,异步任务块不需要等block执行完成就可以返回
    • 因此这里不会发生死锁

    4、 全局并发队列异步

    并发执行任务

    - (void)globalAsyncTest{
        
        for (int i = 0; i<20; i++) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSLog(@"%d-%@",i,[NSThread currentThread]);
            });
        }
        NSLog(@"hello queue");
    }
    

    结果:

    2021-10-31 14:58:16.038522+0800 多线程使用[11062:5684222] hello queue
    2021-10-31 14:58:16.038559+0800 多线程使用[11062:5684360] 0-<NSThread: 0x600003d438c0>{number = 6, name = (null)}
    2021-10-31 14:58:16.038564+0800 多线程使用[11062:5684368] 2-<NSThread: 0x600003d34440>{number = 7, name = (null)}
    2021-10-31 14:58:16.038567+0800 多线程使用[11062:5684359] 3-<NSThread: 0x600003d28780>{number = 4, name = (null)}
    2021-10-31 14:58:16.038571+0800 多线程使用[11062:5684358] 1-<NSThread: 0x600003d282c0>{number = 8, name = (null)}
    2021-10-31 14:58:16.038585+0800 多线程使用[11062:5684361] 4-<NSThread: 0x600003d31b80>{number = 3, name = (null)}
    2021-10-31 14:58:16.038606+0800 多线程使用[11062:5684362] 5-<NSThread: 0x600003d43e00>{number = 5, name = (null)}
    2021-10-31 14:58:16.038642+0800 多线程使用[11062:5684360] 7-<NSThread: 0x600003d438c0>{number = 6, name = (null)}
    

    说明:

    • 异步执行会开启新线程,因此有多个线程来执行
    • 在并发队列中异步执行代码块,因此执行顺序是异步的
    • 并且打印hello是最早执行的,这是因为异步并发执行,异步添加任务消耗的时间比较多,所以先执行hello

    5、 全局并发队列同步

    串行执行任务

    - (void)globalSyncTest{
        for (int i = 0; i<20; i++) {
            dispatch_sync(dispatch_get_global_queue(0, 0), ^{
                NSLog(@"%d-%@",i,[NSThread currentThread]);
            });
        }
        NSLog(@"hello queue");
    }
    

    结果:

    2021-10-31 14:55:53.552472+0800 多线程使用[10971:5681912] 13-<_NSMainThread: 0x600000a2c7c0>{number = 1, name = main}
    2021-10-31 14:55:53.552541+0800 多线程使用[10971:5681912] 14-<_NSMainThread: 0x600000a2c7c0>{number = 1, name = main}
    2021-10-31 14:55:53.552635+0800 多线程使用[10971:5681912] 15-<_NSMainThread: 0x600000a2c7c0>{number = 1, name = main}
    2021-10-31 14:55:53.552729+0800 多线程使用[10971:5681912] 16-<_NSMainThread: 0x600000a2c7c0>{number = 1, name = main}
    2021-10-31 14:55:53.552829+0800 多线程使用[10971:5681912] 17-<_NSMainThread: 0x600000a2c7c0>{number = 1, name = main}
    2021-10-31 14:55:53.559220+0800 多线程使用[10971:5681912] 18-<_NSMainThread: 0x600000a2c7c0>{number = 1, name = main}
    2021-10-31 14:55:53.559332+0800 多线程使用[10971:5681912] 19-<_NSMainThread: 0x600000a2c7c0>{number = 1, name = main}
    2021-10-31 14:55:53.559401+0800 多线程使用[10971:5681912] hello queue
    

    说明:

    • 主队列中有同步任务块和hello两个任务,同步任务块添加了多个任务到并发队列中
    • 因为是同步函数,所以不会开启新线程,只有一个主线程来执行
    • 并且打印hello是在最后执行的,这是因为同步代码块是在hello任务的前面,同步函数需要等代码块执行完成后再返回,所以需要等同步代码块执行完才能执行hello

    简单面试题分析

    面试题1

    - (void)textDemo1{
        
        dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
        NSLog(@"1");
        dispatch_async(queue, ^{
            NSLog(@"2");
            dispatch_sync(queue, ^{
                NSLog(@"3");
            });
            NSLog(@"4");
        });
        NSLog(@"5");
    
    }
    

    结果: 15243

    说明:

    • 在主队列中包含三个任务,同步任务1、异步任务代码块、同步任务5
    • 异步任务块不需要等待函数执行完成就可以返回,而异步函数本身是耗时的,所以前面的顺序是1、5
    • 在异步任务中将三个任务添加到一个并发队列中,包括任务2,同步代码块、任务4
    • 因为是并发队列异步提交任务,所以这三个任务并发执行,
    • 但因为队列的先进先出原则,先进的先调用,再看消耗的时间,同步代码块会消耗更多的时间,所以最后执行
    • 任务2和任务4执行时间一样,但是任务2先执行
    • 所以顺序为2、4、3,合起来就是15243

    面试题2

    - (void)textDemo{
        
        dispatch_queue_t queue = dispatch_queue_create("cooci", NULL);
        NSLog(@"1");
        // 耗时
        dispatch_async(queue, ^{
            NSLog(@"2");
            dispatch_sync(queue, ^{
                NSLog(@"3");
            });
            NSLog(@"4");
        });
        NSLog(@"5");
    }
    

    结果: 1、5、2,崩溃

    20-面试题解析2.png

    说明:

    • 在主队列中包含三个任务,同步任务1、异步任务代码块、同步任务5
    • 主队列是串行的,所以调度顺序是任务1、代码块、任务5
    • 异步任务块不需要等block执行完就可以返回,所以顺序是1、5
    • 代码块中将三个任务添加到一个并发队列中,包括任务2,同步代码块、任务4
    • 在串行队列中,包含任务2和同步任务块、任务4、任务3,而同步任务块的执行结束需要等待任务3的结束
    • 造成死锁,因此在执行完3后会崩溃

    注意:
    这里虽然是主队列,任务块在任务5的前面,但是在执行时我们看到先执行了任务5后执行了任务2,
    这是因为异步操作并不需要等待函数的执行完成而完成,

    面试题3

    - (void)wbinterDemo{
        dispatch_queue_t queue11 = dispatch_queue_create("wy", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_async(queue11, ^{
            NSLog(@"1");
        });
        dispatch_async(queue11, ^{
            NSLog(@"2");
        });
        dispatch_sync(queue11, ^{
            NSLog(@"3");
        });
        NSLog(@"0");
        dispatch_async(queue11, ^{
            NSLog(@"1");
        });
        dispatch_async(queue11, ^{
            NSLog(@"7");
        });
        dispatch_async(queue11, ^{
            NSLog(@"8");
        });
        dispatch_async(queue11, ^{
            NSLog(@"9");
        }); 
    }
    

    请选择:
    A:1230789
    B:1237890
    C:3120798
    D:2137890

    答案为AC

    说明:

    • 首先这些任务块操作都是在主队列中,其中任务3不会开启新线程,和任务0都是主线程执行的,其他的任务在并发队列中,会开启新线程执行,同步任务块必须等任务3执行完才能继续往下执行,所以任务3肯定在任务0前面。(简单认识,同步并发是串行,所以任务3在任务0前面)
    • 7、8、9任务在任务0后添加,所以会在0任务后面执行
    • 同时7、8、9任务都是异步并发,所以并发执行,没有顺序
    • 任务1、2在任务3之前添加到并发队列中,也就是任务1和2与3在不同的队列中,而任务1和2又是异步并发的,所以他们三个会异步并发执行,他们三个是没有顺序的。

    死锁的认识

    死锁就是同一个串行队列中两个任务相互等待引起死锁.

    网上面很多说是不同的线程相互等待引起的,是解释不通的,应该是队列,因为队列是先进先出原则导致,而后面进入的任务的结束又依赖于前面进的任务。
    一个串行队列的线程池只维护了一个线程。

    死锁的场景

    主队列同步执行

    - (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"任务1");
    });
    }
    

    说明:

    • 在主队列中有同步任务块,之后添加任务1到主队列中。
    • 而同步任务块的执行完成需要等block执行完成,所以同步任务块的执行依赖于任务1
    • 而在串行队列中,任务1的完成又需要同步任务块的完成。
    • 由此同步任务块和任务1相互等待,造成了死锁

    同步串行队列的嵌套场景也是一样的,不再多言

    - (void)textDemo{
        
        dispatch_queue_t queue = dispatch_queue_create("cooci", NULL);
        NSLog(@"1");
        // 耗时
        dispatch_async(queue, ^{
            NSLog(@"2");
            dispatch_async(queue, ^{
                NSLog(@"3");
            });
            NSLog(@"4");
        });
        NSLog(@"5");
    }
    

    死锁的解决

    上面知道死锁是一个串行队列中有两个任务相互等待导致的,所以有两个解决办法,

    • 队列为并发,不是串行
    • 异步任务,取消一方的等待,也就打破相互等待

    所以可以有两个做法,

    将队列改为并发

    - (void)textDemo1{
        
        dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
        NSLog(@"1");
        dispatch_async(queue, ^{
            NSLog(@"2");
            dispatch_sync(queue, ^{
                NSLog(@"3");
            });
            NSLog(@"4");
        });
        NSLog(@"5");
    
    }
    

    将任务改为异步任务,及不会相互等待

    - (void)textDemo{
        
        dispatch_queue_t queue = dispatch_queue_create("cooci", NULL);
        NSLog(@"1");
        // 耗时
        dispatch_async(queue, ^{
            NSLog(@"2");
            dispatch_async(queue, ^{
                NSLog(@"3");
            });
            NSLog(@"4");
        });
        NSLog(@"5");
    }
    

    相关文章

      网友评论

          本文标题:09 - OC多线程之队列任务以及死锁的认识

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