美文网首页
GCD ( 一 ) :多线程,任务,队列,同步异步

GCD ( 一 ) :多线程,任务,队列,同步异步

作者: 司空123 | 来源:发表于2019-05-25 00:43 被阅读0次

    一、简介:

    GCD(Grand Central Dispatch)是苹果推出的一种多线程技术,在iOS4及以上版本使用.
    好处:
    1.利用多核CPU的优势,进行并行计算
    2.自动管理线程的生命周期,线程的创建、销毁以及任务调度,不需要编写管理线程的代码

    几个需要掌握并深刻理解的概念:
    线程:一条执行任务的通道.

    任务:就是你要执行的代码片段.

    同步与异步:任务的执行方式,主要决定的是能不能开启新线程.

    同步表示在当前线程中执行任务,异步表示在新的线程中执行任务,不阻塞当前线程,同步不具备开启新线程的能力,异步具备开新线程的能力,但是,不一定真的开启新线程,由队列的类型决定.

    队列:(Queues)存放待执行任务的排队队列.任务的执行有队列进行分发.

    1.队列管理你提供给 GCD 的任务并用 FIFO 顺序执行这些任务。这就保证了第一个被添加到队列里的任务会是队列中第一个开始的任务,而第二个被添加的任务将第二个开始,如此直到队列的终点。
    2.所有的调度队列(dispatch queues)自身都是线程安全的,你能从多个线程并行的访问它们。当你了解了调度队列如何为你自己代码的不同部分提供线程安全后,GCD的优点就是显而易见的。关键是选择正确类型的调度队列和正确的调度函数来执行你的任务。
    3.GCD提供两种调度队列,串行队列,并发队列(不是并行队列)

    串行队列:串行队列中的任务是一个接一个执行one by one,每个任务只在前一个任务完成时才开始。

    这些任务的执行时机受到 GCD 的控制;唯一能确保的事情是 GCD 一次只执行一个任务,并且按照我们添加到队列的顺序来执行。

    由于在串行队列中不会有两个任务并发运行,因此不会出现同时访问临界区的风险;相对于这些任务来说,这就从竞态条件下保护了临界区。所以如果访问临界区的唯一方式是通过提交到调度队列的任务,那么你就不需要担心临界区的安全问题了

    并发队列在并发队列中的任务能得到的保证是它们会按照被添加的顺序(FIFO)开始执行。但任务可能以任意顺序完成,你不会知道何时开始运行下一个任务,或者任意时刻有多少 Block 在运行。这完全取决于 GCD 。

    并发与并行:并发和并行通常被一起提到,所以值得花些时间解释它们之间的区别。

    并发是队列中任务可以执行在多条通道中,具体由系统派发.多核设备通过并行来同时执行多个线程,单核设备也能实现这一点,它们必须先运行一个线程,执行一个上下文切换,然后运行另一个线程或进程.
    并行是指你的多个工作片段,同时在执行.例如,你需要下载三张图片,那么三张图片分别在下载并且进度独立,就是并行.

    虽然你可以编写代码在 GCD 下并发执行,但 GCD 会决定有多少并行的需求。并行要求并发,但并发并不能保证并行

    二、基本用法:

    创建队列:这里使用 dispatch_queue_create 初始化一个队列。第一个参数是反向DNS样式命名惯例;确保它是描述性的,将有助于调试。第二个参数指定你的队列是串行还是并发。

    1.串行队列:

    //创建串行队列:  
    dispatch_queue_t queue = dispatch_queue_create("com.Test.queue", DISPATCH_QUEUE_SERIAL);
    

    注意:你会经常看人们传递 0 或者 NULL 给 dispatch_queue_create 的第二个参数。这是一个创建串行队列的过时方式;明确你的参数总是更好。

    2.并发队列:

    // 创建并发队列:
    dispatch_queue_t queue = dispatch_queue_create("com.Test.queue", DISPATCH_QUEUE_CONCURRENT);
    

    3.获取主队列

     // 获取主队列(主队列是串行队列)
        dispatch_queue_t queue = dispatch_get_main_queue();
    

    主队列(main queue) 是由系统提供给你一个特殊队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。然而,它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。这个队列就是用于发生消息给 UIView 或发送通知的。

    4.获取全局并发队列
    GCD提供了一个全局的并发队列,使用dispatch_get_global_queue获取,需要传入两个参数,第一个参数是队列的优先级,第二个是预留参数,传0即可

     // 获取全局并发队列
     dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
    

    全局队列有着不同的优先级:background、low、default 以及 high。要知道,Apple 的 API 也会使用这些队列,所以你添加的任何任务都不会是这些队列中唯一的任务。

    创建执行任务的方法:
    1.同步任务:

       dispatch_sync(queue, ^{
            // 同步任务执行的代码
        });
    

    2.异步任务:

       dispatch_async(queue, ^{
            // 异步任务执行的代码
        });
    

    编写GCD代码:编写GCD代码要指明任务执行的方式(同步还是异步),任务执行的线程.因此,会有如下几种情况:
    1.串行队列,同步任务
    2.串行队列,异步任务
    2.并发队列,同步任务
    4.并发队列,异步任务
    5.主队列,同步任务
    6.主队列,异步任务
    7.全局并发队列,同步任务
    8.全局并发队列,异步任务

    不管我们选择以何种方式编写代码,我们的初衷是能清楚的知道自己的代码片段是如何在线程中被分发被执行的(清楚自己代码片段的耗时时间的前提下),我们想明确几个指导原则:
    1.同步任务是不会开启线程的;
    2.异步任务具备开启线程的能力,但要看队列是否是主队列,线程的具体开辟由GCD决定;
    3.主队列dispatch_get_main_queue是串行的,队列中的任务,在主线程中执行;

    1. dispatch_get_global_queue是全局并发队列,具有四种优先级别;

    我们先验证上面几种组合的执行情况:

    1.串行队列,同步任务

        dispatch_queue_t serialQueue = dispatch_queue_create("com.testQueue.serial", DISPATCH_QUEUE_SERIAL);
        dispatch_sync(serialQueue, ^{
            [self timeMethod:@"串行队列--同步任务1"];
        });
        dispatch_sync(serialQueue, ^{
            [self timeMethod:@"串行队列--同步任务2"];
        });
    //    2019-05-15 17:04:06.805232+0800 GCDDemo[61363:3115102] 串行队列--同步任务1---<NSThread: 0x6000020808c0>{number = 1, name = main}
    //    2019-05-15 17:04:07.806662+0800 GCDDemo[61363:3115102] 串行队列--同步任务1---<NSThread: 0x6000020808c0>{number = 1, name = main}
    //    2019-05-15 17:04:08.807174+0800 GCDDemo[61363:3115102] 串行队列--同步任务2---<NSThread: 0x6000020808c0>{number = 1, name = main}
    //    2019-05-15 17:04:09.808687+0800 GCDDemo[61363:3115102] 串行队列--同步任务2---<NSThread: 0x6000020808c0>{number = 1, name = main}
    
    

    上面代码的执行过程:
    1.同步任务,不会开辟线程,运行在当前线程;
    2.当前线程为主线程;
    3.串行队列,所以任务是顺序执行的
    结论:没有开启新线程 ,串行执行,主线程中执行

    2.串行队列,异步任务

        dispatch_queue_t serialQueue = dispatch_queue_create("com.testQueue.serial", DISPATCH_QUEUE_SERIAL);
    
        dispatch_async(serialQueue, ^{
            [self timeMethod:@"串行队列--异步任务1"];
        });
    
        dispatch_async(serialQueue, ^{
            [self timeMethod:@"串行队列--异步任务2"];
        });
        
    //    2019-05-15 17:13:08.344125+0800 GCDDemo[61652:3122985] 串行队列--异步任务1---<NSThread: 0x6000014cc2c0>{number = 3, name = (null)}
    //    2019-05-15 17:13:10.347402+0800 GCDDemo[61652:3122985] 串行队列--异步任务1---<NSThread: 0x6000014cc2c0>{number = 3, name = (null)}
    //    2019-05-15 17:13:12.347897+0800 GCDDemo[61652:3122985] 串行队列--异步任务2---<NSThread: 0x6000014cc2c0>{number = 3, name = (null)}
    //    2019-05-15 17:13:14.353511+0800 GCDDemo[61652:3122985] 串行队列--异步任务2---<NSThread: 0x6000014cc2c0>{number = 3, name = (null)}
    

    上面代码的执行过程:
    1.异步任务,具备开辟线程能力;
    3.串行队列,所以任务是顺序执行的
    结果:开启新线程,串行执行

    3.并发队列,同步任务

        dispatch_queue_t concunrrentQueue = dispatch_queue_create("com.testQueue.CONCURRENT", DISPATCH_QUEUE_CONCURRENT);
        dispatch_sync(concunrrentQueue, ^{
            [self timeMethod:@"并发队列--同步任务1"];
        });
        dispatch_sync(concunrrentQueue, ^{
            [self timeMethod:@"并发队列--同步任务2"];
        });
    
    //    2019-05-15 17:15:33.557345+0800 GCDDemo[61750:3125543] 并发队列--同步任务1---<NSThread: 0x6000007fd400>{number = 1, name = main}
    //    2019-05-15 17:15:35.558439+0800 GCDDemo[61750:3125543] 并发队列--同步任务1---<NSThread: 0x6000007fd400>{number = 1, name = main}
    //    2019-05-15 17:15:37.559989+0800 GCDDemo[61750:3125543] 并发队列--同步任务2---<NSThread: 0x6000007fd400>{number = 1, name = main}
    //    2019-05-15 17:15:39.561567+0800 GCDDemo[61750:3125543] 并发队列--同步任务2---<NSThread: 0x6000007fd400>{number = 1, name = main}
    

    上面代码的执行过程:
    1.同步任务,不会开辟线程,运行在当前线程;
    2.当前线程为主线程;
    3.并发队列,但由于任务是同步的,所以任务是顺序执行的
    结果:没有开启新线程 ,串行执行,主线程执行

    4.并发队列,异步任务

        dispatch_queue_t concunrrentQueue = dispatch_queue_create("com.testQueue.CONCURRENT", DISPATCH_QUEUE_CONCURRENT);
    
        dispatch_async(concunrrentQueue, ^{
            [self timeMethod:@"并发队列--异步任务1"];
        });
        dispatch_async(concunrrentQueue, ^{
            [self timeMethod:@"并发队列--异步任务2"];
        });
        
    //    2019-05-15 17:14:39.933850+0800 GCDDemo[61719:3124710] 并发队列--异步任务1---<NSThread: 0x600000e56800>{number = 3, name = (null)}
    //    2019-05-15 17:14:39.933849+0800 GCDDemo[61719:3124711] 并发队列--异步任务2---<NSThread: 0x600000e56a80>{number = 4, name = (null)}
    //    2019-05-15 17:14:41.936166+0800 GCDDemo[61719:3124711] 并发队列--异步任务2---<NSThread: 0x600000e56a80>{number = 4, name = (null)}
    //    2019-05-15 17:14:41.936166+0800 GCDDemo[61719:3124710] 并发队列--异步任务1---<NSThread: 0x600000e56800>{number = 3, name = (null)}
    

    上面代码的执行过程:
    1.异步任务,具备开启线程能力;
    2.并发队列,异步任务,并发执行
    3.线程开辟情况由GCD确定,任务执行顺序由任务本身耗时情况决定.
    结果:开启新线程,并发执行

    5.主线程同步任务

    - (void)sysMain{
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"---%@",[NSThread currentThread]);
        });
    }
    

    上面代码的执行过程:
    1.主队列是串行队列,而且在主线程;
    2.添加同步任务,同步任务需要等待主线程中的任务全部执行完再执行当前任务,
    结果:发生死锁

    6.主队列,异步任务

        dispatch_async(dispatch_get_main_queue(), ^{
            [self timeMethod:@"主队列--异步任务1"];
        });
    
        dispatch_async(dispatch_get_main_queue(), ^{
            [self timeMethod:@"主队列--异步任务2"];
        });
        
    //    2019-05-15 17:19:15.305626+0800 GCDDemo[61873:3128986] 主队列--异步任务1---<NSThread: 0x600002fa6940>{number = 1, name = main}
    //    2019-05-15 17:19:17.307348+0800 GCDDemo[61873:3128986] 主队列--异步任务1---<NSThread: 0x600002fa6940>{number = 1, name = main}
    //    2019-05-15 17:19:19.308964+0800 GCDDemo[61873:3128986] 主队列--异步任务2---<NSThread: 0x600002fa6940>{number = 1, name = main}
    //    2019-05-15 17:19:21.310305+0800 GCDDemo[61873:3128986] 主队列--异步任务2---<NSThread: 0x600002fa6940>{number = 1, name = main}
    

    上面代码的执行过程:
    1.异步任务,有开辟线程的能力;
    2.主队列在主线程中运行,所以不开启新线程;
    3.主队列为串行队列,所以任务串行执行.
    结果:没有开启新线程,串行执行

    7.全局并发队列,同步任务

        dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self timeMethod:@"全局并发队列--同步任务1"];
        });
    
        dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self timeMethod:@"全局并发队列--同步任务2"];
        });
        
    //    2019-05-15 17:21:56.781514+0800 GCDDemo[61953:3131089] 全局并发队列--同步任务1---<NSThread: 0x600002be9300>{number = 1, name = main}
    //    2019-05-15 17:21:58.783111+0800 GCDDemo[61953:3131089] 全局并发队列--同步任务1---<NSThread: 0x600002be9300>{number = 1, name = main}
    //    2019-05-15 17:22:00.784664+0800 GCDDemo[61953:3131089] 全局并发队列--同步任务2---<NSThread: 0x600002be9300>{number = 1, name = main}
    //    2019-05-15 17:22:02.786188+0800 GCDDemo[61953:3131089] 全局并发队列--同步任务2---<NSThread: 0x600002be9300>{number = 1, name = main}
    

    上面代码的执行过程:
    1.同步任务,不开启新线程,在当前线程执行;
    2.全局并发队列是并发队列,但任务为同步,所以串行执行'

    结果:没有开启新线程 ,串行执行,主线程

    8.全局并发队列,异步任务

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self timeMethod:@"全局并发队列--异步任务1"];
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self timeMethod:@"全局并发队列--异步任务2"];
        });
        
    //    2019-05-15 17:23:37.208830+0800 GCDDemo[62018:3132621] 全局并发队列--异步任务1---<NSThread: 0x600000baad00>{number = 4, name = (null)}
    //    2019-05-15 17:23:37.208892+0800 GCDDemo[62018:3132622] 全局并发队列--异步任务2---<NSThread: 0x600000baacc0>{number = 3, name = (null)}
    //    2019-05-15 17:23:39.213383+0800 GCDDemo[62018:3132621] 全局并发队列--异步任务1---<NSThread: 0x600000baad00>{number = 4, name = (null)}
    //    2019-05-15 17:23:39.213384+0800 GCDDemo[62018:3132622] 全局并发队列--异步任务2---<NSThread: 0x600000baacc0>{number = 3, name = (null)}
    

    上面代码的执行过程:
    1.异步任务,有开启线程能力;
    2.全局并发队列,开启新线程,并发执行
    结果:开启新线程,并发执行

    运行结果统计在下表:

    区别 并发队列 串行队列 主队列 全局并发队列
    同步(sync) 没有开启新线程 ,串行执行 没有开启新线程,串行执行 主线程调用,发生死锁 没有开启新线程 ,串行执行,主线程
    异步(async) 有开启新线程,并发执行 有开启新线程,串行执行 没有开启新线程,串行执行 开启新线程,并发执行

    从上表可以得出结论:
    1.同步(sync),不具备开启线程的条件;
    2.异步(async),具备开启线程的条件,但并不一定开启新线程,这跟任务指定的队列类型有关.
    3.主线程同步会发生死锁.
    4.主队列是串行队列,但不同于自定义的串行队列.
    5.全局并发队列是并发队列,同自定义并发队列情况一致.
    6.并发队列,只有在异步时才会真正开启线程并发执行.

    至此,大致的组合情况已经清楚了,但是还有一些情况需要验证,
    比如:
    1.在子线程中执行主队列同步任务会怎样?是否会死锁?

    - (void)subThreadSyncMain{
        
        [NSThread detachNewThreadWithBlock:^{
            dispatch_sync(dispatch_get_main_queue(), ^{
                NSLog(@"1---%@",[NSThread currentThread]);
            });
            
            dispatch_sync(dispatch_get_main_queue(), ^{
                NSLog(@"2---%@",[NSThread currentThread]);
            });
        }];
    
    //    2019-05-16 14:22:47.080669+0800 GCDDemo[97183:4202776] 1---<NSThread: 0x60000239d380>{number = 1, name = main}
    //    2019-05-16 14:22:47.081528+0800 GCDDemo[97183:4202776] 2---<NSThread: 0x60000239d380>{number = 1, name = main}
    
    }
    
    - (void)subThreadAsyncMain{
        
        [NSThread detachNewThreadWithBlock:^{
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"1---%@",[NSThread currentThread]);
            });
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"2---%@",[NSThread currentThread]);
            });
        }];
    //    2019-05-16 14:23:46.261385+0800 GCDDemo[97249:4208648] 1---<NSThread: 0x600000941440>{number = 1, name = main}
    //    2019-05-16 14:23:46.261564+0800 GCDDemo[97249:4208648] 2---<NSThread: 0x600000941440>{number = 1, name = main}
    }
    

    结果:没有发生死锁,任务顺序执行,并且在主线程中执行.原因是主队列中的任务都在主线程中执行

    思考一下下面的执行顺序是怎样的?

    - (void)subThreadSysSerianl{
        
        dispatch_queue_t queue = dispatch_queue_create("com.test", DISPATCH_QUEUE_SERIAL);// 1
        [NSThread detachNewThreadWithBlock:^{//2
            dispatch_sync(queue, ^{//3
                NSLog(@"1");
            });
            dispatch_sync(queue, ^{//4
                NSLog(@"2");
            });
        }];
    }
    

    答案是: 1,2
    上面代码的执行过程:
    1.创建一个串行队列;
    2.开启一个子线程;
    3.在当前队列中添加任务1;
    4.在当前队列中添加任务2;
    由于是串行队列同步任务,队列按照FIFO的顺序执行,所以任务1和任务2顺序执行.
    那么将任务二放在任务一中,会发生什么?

    - (void)subThreadSysSerianl{
        dispatch_queue_t queue = dispatch_queue_create("com.test", DISPATCH_QUEUE_SERIAL);// 1
        [NSThread detachNewThreadWithBlock:^{//2
            dispatch_sync(queue, ^{//3
                NSLog(@"1---%@",[NSThread currentThread]);
                dispatch_sync(queue, ^{//4
                    NSLog(@"2---%@",[NSThread currentThread]);
                });
            });
        }];
    }
    

    会发生死锁:
    我们来捋顺一下执行过程:
    1.前提是串行队列同步任务,这很重要;
    2.然后先将任务1放进队列中;
    3.任务1先执行一步打印输出1---;
    4.然后执行的是异步任务2;
    5.根据串行队列同步任务的特点,执行到任务2时,任务2要等待任务1执行完成才可以执行,而任务1又在等待任务2执行完成.因此发生死锁.
    6.对比上面发生的主队列同步任务发生死锁,就更好理解了.

    相关文章

      网友评论

          本文标题:GCD ( 一 ) :多线程,任务,队列,同步异步

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