GCD浅析

    xiaoxiao2021-03-25  45

    1.关于GCD

    Grand Central Dispatch是异步执行任务的技术之一。我们先看一个简单的示例:

    1 - (void)doSomethingInBackground { 2 [self performSelectorInBackground:@selector(startWork) withObject:nil]; 3 } 4 5 - (void)startWork { 6 //长时间处理操作 7 NSLog(@"进行长时间处理"); 8 9 [self performSelectorOnMainThread:@selector(endWork) withObject:nil waitUntilDone:YES]; 10 } 11 12 - (void)endWork { 13 NSLog(@"完成工作并刷新页面"); 14 }

    这个示例实现的功能很简单:在后台线程中执行长时间的处理,处理结束后,在主线程中使用处理结果。上面实现方式是使用NSObject类的performSelectorInBackground和performSelectorOnMainThread来实现的。下面我们用GCD的方式来实现同样功能,做个对比:

    1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT); 2 dispatch_async(queue, ^{ 3 NSLog(@"进行长时间处理"); 4 dispatch_async(dispatch_get_main_queue(), ^{ 5 NSLog(@"完成工作并刷新页面"); 6 }); 7 });

    通过上面的比较,我们可以知道,GCD更加的简洁,同样的功能,代码量减少了差不多一半。

    2.多线程编程

    什么是线程?线程(英语:thread)是操作系统能够进行运算调度的最小单位(来自维基百科)。iOS应用程序在启动后,首先会将包含在应用程序中的CPU指令配置到内存中,CPU从应用程序指定的地址开始,一个一个去执行CPU指令,一个CPU一次只能执行一个指令。由于现在的物理CPU芯片实际上有64个(64核)CPU,如果1个CPU核虚拟为两个CPU核工作,那么使用多个CPU核同时工作就是很正常的事情了。这种存在多个CPU核工作的即为“多线程”。

    在实际开发中,多线程编程是一种容易发生问题的编程技术。我们常见的有这么几种:

    数据竞争:多个线程更新相同的资源会导致数据的不一致; 死锁:多个线程相互持续等待; 内存消耗高:每个线程都是有内存开销的,太多线程会大量的消耗内存。

    多线程存在上面的这些问题,那么我们是不是应该弃用呢?答案是否定的,因为使用多线程编程,可以让我们在执行长时间的处理时,保证用户界面的即时响应。

    3.GCD的API

    3.1Dispatch Queue

    分发队列主要有两类:

    Serial Dispatch Queue:等待现在执行任务处理结束; Concurrent Dispatch Queue:不等待现在执行处理结束;

    听概念是不是感觉很抽象?没关系,下面我们来看看代码。

    1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL); 2 3 dispatch_async(queue, ^{ 4 NSLog(@"blk1"); 5 }); 6 7 dispatch_async(queue, ^{ 8 NSLog(@"blk2"); 9 }); 10 11 dispatch_async(queue, ^{ 12 NSLog(@"blk3"); 13 }); 14 15 dispatch_async(queue, ^{ 16 NSLog(@"blk4"); 17 }); 18 19 dispatch_async(queue, ^{ 20 NSLog(@"blk5"); 21 });

    可以看到,上面我们使用的是Serial Dispatch Queue,运行效果会怎么样呢?

    可以看到:使用Serial Dispatch Queue,因为要等待正在执行的任务结束,并且同事执行的处理数只能有一个,所以会依次打印blk1、blk2、blk3、blk4、blk5。

    现在将上面示例的DISPATCH_QUEUE_SERIAL改为DISPATCH_QUEUE_CONCURRENT,结果会怎么样呢?

    结论是:使用Concurrent Dispatch Queue时,因为不用等待正在执行的任务结束,可以并行执行多个任务,也就是使用多个线程同时执行,但并行执行任务的数量取决于系统当前的状态。上面并行执行的线程情况截图如下:

    3.2dispatch_queue_create

    上面介绍了dispatch queue,那么怎么创建分发队列呢?我们可以使用GCD的API函数。

    1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL);

    dispatch_queue_create函数的第一个参数指定队列的名称,这个名称会在XCode调试中显示,也会显示在应用程序崩溃时所生成的CrashLog中。因此我们命名最好时取一些有意义的,以方便我们来定位问题和代码。例如下面的代码:

    1 dispatch_queue_t queue1 = dispatch_queue_create("com.gcd.test1", DISPATCH_QUEUE_SERIAL); 2 dispatch_queue_t queue2 = dispatch_queue_create("com.gcd.test2", DISPATCH_QUEUE_SERIAL); 3 dispatch_queue_t queue3 = dispatch_queue_create("com.gcd.test3", DISPATCH_QUEUE_SERIAL); 4 dispatch_queue_t queue4 = dispatch_queue_create("com.gcd.test4", DISPATCH_QUEUE_SERIAL); 5 dispatch_queue_t queue5 = dispatch_queue_create("com.gcd.test5", DISPATCH_QUEUE_SERIAL); 6 7 dispatch_async(queue1, ^{ 8 NSLog(@"blk1"); 9 }); 10 11 dispatch_async(queue2, ^{ 12 NSArray *arr = @[@1, @2, @3]; 13 NSLog(@"%@", arr[4]); 14 }); 15 16 dispatch_async(queue3, ^{ 17 NSLog(@"blk3"); 18 }); 19 20 dispatch_async(queue4, ^{ 21 NSLog(@"blk4"); 22 }); 23 24 dispatch_async(queue5, ^{ 25 NSLog(@"blk5"); 26 });

    从代码可以看到,queue2会崩溃,实际运行后的CrashLog如下:

    dispatch_queue_create函数的第二个参数指定创建队列的类型。如果想创建Serial Dispatch Queue,那么指定类型为DISPATCH_QUEUE_SERIALNULL;如果想创建Concurrent Dispatch Queue,那么指定类型为DISPATCH_QUEUE_CONCURRENT

    【注意】:dispatch_queue_create函数可以创建任意个Dispatch Queue,当生成多个Serial Dispatch Queue时,各个Serial Dispatch Queue会并行执行,系统对于每一个Serial Dispatch Queue会生成并使用一个线程。如果生成大量的Serial Dispatch Queue,那么就会创建很多线程。过多使用线程,会消耗大量内存,降低系统的响应性能。

    3.3Main Dispatch Queue/Global Dispatch Queue

     上面说了用dispatch_queue_create函数生成Dispatch Queue,现在聊一下系统标准提供的几个Dispatch Queue。

    名称Dispatch Queue类型说明Main Dispatch QueueSerial Dispatch Queue主线程执行Global Dispatch Queue(High Priority)Concurrent Dispatch Queue执行优先级:高Global Dispatch Queue(Default Priority)Concurrent Dispatch Queue执行优先级:默认Global Dispatch Queue(Low Priority)Concurrent Dispatch Queue执行优先级:低Global Dispatch Queue(Background Priority)Concurrent Dispatch Queue执行优先级:后台

    怎么创建?

    1 dispatch_queue_t mainQueue = dispatch_get_main_queue(); 2 dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); 3 dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 4 dispatch_queue_t lowQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); 5 dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

    3.4dispatch_after

    从这个函数名称,我们也能猜到它的大概作用:在xx时间后,再做某件事情。

    NSLog(@"Before:%@", [NSDate date]); dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC); dispatch_after(time, dispatch_get_main_queue(), ^{ NSLog(@"After:%@", [NSDate date]); });

    【注意】:dispatch_after函数并不是在指定时间后执行处理,而只是在指定时间后追加处理(也就是Block)到Dispatch Queue。

    3.5Dispatch_Group

    假设现在有个新的需求:需要在 Dispatch Queue中的多个任务完成之后,再做某件事情。当然大家会马上反应过来,用Serial Dispatch Queue即可。但如果应用场景是Concurrent Dispatch Queue或者多个Dispatch Queue呢?

    这个时候就需要Dispatch Group了。

    1 dispatch_queue_t queue1 = dispatch_queue_create("com.gcd.test1", DISPATCH_QUEUE_SERIAL); 2 dispatch_queue_t queue2 = dispatch_queue_create("com.gcd.test2", DISPATCH_QUEUE_SERIAL); 3 dispatch_queue_t queue3 = dispatch_queue_create("com.gcd.test3", DISPATCH_QUEUE_SERIAL); 4 dispatch_queue_t queue4 = dispatch_queue_create("com.gcd.test4", DISPATCH_QUEUE_SERIAL); 5 dispatch_queue_t queue5 = dispatch_queue_create("com.gcd.test5", DISPATCH_QUEUE_SERIAL); 6 7 dispatch_group_t group = dispatch_group_create(); 8 dispatch_group_async(group, queue1, ^{ 9 NSLog(@"blk1"); 10 }); 11 dispatch_group_async(group, queue2, ^{ 12 NSLog(@"blk2"); 13 }); 14 dispatch_group_async(group, queue3, ^{ 15 NSLog(@"blk3"); 16 }); 17 dispatch_group_async(group, queue4, ^{ 18 NSLog(@"blk4"); 19 }); 20 dispatch_group_async(group, queue5, ^{ 21 NSLog(@"blk5"); 22 }); 23 24 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ 25 NSLog(@"done"); 26 });

    多次运行,我们发现打印done总是在最后:

    3.6dispatch_barrier_async

    闭上眼睛,来想象这么一个场景:假设我们有一个本地数据库,会频繁的去读取数据和写入数据;写入是一个特殊的操作,如果在写入的时候有其他的读取操作,那么很有可能会读到脏数据,甚至也可能会造成程序的崩溃;如果只是单纯的读取操作,那么多个并行就不会发生问题。

    针对这种场景,我们该怎么办呢?

    1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT); 2 3 dispatch_async(queue, ^{ 4 NSLog(@"blk1"); 5 }); 6 dispatch_async(queue, ^{ 7 NSLog(@"blk2"); 8 }); 9 dispatch_async(queue, ^{ 10 NSLog(@"blk3"); 11 }); 12 dispatch_barrier_async(queue, ^{ 13 NSLog(@"blk0"); 14 }); 15 dispatch_async(queue, ^{ 16 NSLog(@"blk4"); 17 }); 18 dispatch_async(queue, ^{ 19 NSLog(@"blk5"); 20 }); 21 dispatch_async(queue, ^{ 22 NSLog(@"blk6"); 23 });

    多次运行看结果:

    通过多次运行,我们发现,不管怎么变化,blk0的打印永远在blk1、blk2、blk3的后面,blk4、blk5、blk6的前面。

    3.7dispatch_sync

    前面一直在说的都是“async”,意思就是非同步的将Block追加到指定的Dispatch Queue,dispatch_async函数不做任何等待;

    对应的就有“sync”,意思就是同步的将Block追加到指定的Dispatch Queue。在追加Block结束之前,dispatch_sync函数会一直等待。

    1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT); 2 dispatch_sync(queue, ^{ 3 NSLog(@"blk1"); 4 }); 5 dispatch_sync(queue, ^{ 6 NSLog(@"blk2"); 7 }); 8 dispatch_sync(queue, ^{ 9 NSLog(@"blk3"); 10 }); 11 dispatch_sync(queue, ^{ 12 NSLog(@"blk4"); 13 }); 14 dispatch_sync(queue, ^{ 15 NSLog(@"blk5"); 16 }); 17 18 NSLog(@"主线程");

    多次运行看看结果,发现结果总是固定的,另外查看线程,执行都是在主线程。

    如果使用dispatch_async呢?结果怎样?多次运行,可以发现顺序是随机的。

    学到这里,我们已经接触到了这么4个概念:同步、异步、串行、并行。下面总结一下:

    队列分为串行和并行,任务的执行分为同步和异步,异步是多线程的代名词,异步在实际应用中会开启新的线程,执行耗时操作; 队列只是负责任务的调度,而不负责任务的执行,任务是在线程中执行。 同步意味着在当前线程中执行,异步意味着会开启新的线程。

    下面我们继续来细细品味一下这4个概念:串行同步、串行异步、并行同步、并行异步。

     3.7.1串行同步

    1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL); 2 dispatch_sync(queue, ^{ 3 NSLog(@"blk1 thread:%@", [NSThread currentThread]); 4 }); 5 dispatch_sync(queue, ^{ 6 NSLog(@"blk2 thread:%@", [NSThread currentThread]); 7 }); 8 dispatch_sync(queue, ^{ 9 NSLog(@"blk3 thread:%@", [NSThread currentThread]); 10 }); 11 dispatch_sync(queue, ^{ 12 NSLog(@"blk4 thread:%@", [NSThread currentThread]); 13 }); 14 dispatch_sync(queue, ^{ 15 NSLog(@"blk5 thread:%@", [NSThread currentThread]); 16 }); 17 18 NSLog(@"主线程 thread:%@", [NSThread currentThread]);

    运行结果:

    结论:取出一个任务不放进别的线程,阻塞当前线程(不开辟新的线程,顺序执行),等任务执行完成,开始下一个任务。

     3.7.2串行异步

    1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL); 2 dispatch_async(queue, ^{ 3 NSLog(@"blk1 thread:%@", [NSThread currentThread]); 4 }); 5 dispatch_async(queue, ^{ 6 NSLog(@"blk2 thread:%@", [NSThread currentThread]); 7 }); 8 dispatch_async(queue, ^{ 9 NSLog(@"blk3 thread:%@", [NSThread currentThread]); 10 }); 11 dispatch_async(queue, ^{ 12 NSLog(@"blk4 thread:%@", [NSThread currentThread]); 13 }); 14 dispatch_async(queue, ^{ 15 NSLog(@"blk5 thread:%@", [NSThread currentThread]); 16 }); 17 18 NSLog(@"主线程 thread:%@", [NSThread currentThread]);

    运行结果:

    结论:取出一个任务放进别的线程,不阻塞当前线程,等任务执行完成,开始下一个任务。 

    3.7.3并行同步

    1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT); 2 dispatch_sync(queue, ^{ 3 NSLog(@"blk1 thread:%@", [NSThread currentThread]); 4 }); 5 dispatch_sync(queue, ^{ 6 NSLog(@"blk2 thread:%@", [NSThread currentThread]); 7 }); 8 dispatch_sync(queue, ^{ 9 NSLog(@"blk3 thread:%@", [NSThread currentThread]); 10 }); 11 dispatch_sync(queue, ^{ 12 NSLog(@"blk4 thread:%@", [NSThread currentThread]); 13 }); 14 dispatch_sync(queue, ^{ 15 NSLog(@"blk5 thread:%@", [NSThread currentThread]); 16 }); 17 18 NSLog(@"主线程 thread:%@", [NSThread currentThread]);

    运行结果:

    结论:取出一个任务不放进别的线程,阻塞当前线程(不开辟新的线程,顺序执行),等任务执行完成,开始下一个任务。

    3.7.4并行异步

    dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ NSLog(@"blk1 thread:%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"blk2 thread:%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"blk3 thread:%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"blk4 thread:%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"blk5 thread:%@", [NSThread currentThread]); }); NSLog(@"主线程 thread:%@", [NSThread currentThread]);

    运行结果:

    结论:取出一个任务放进别的线程,不阻塞当前线程,不等任务执行完成,开始下一个任务(会开启多个线程)。

    3.7.5总结

     串行(Serial Dispatch Queue)并行(Concurrent Dispatch Queue)主线程(Main Queue)同步(sync)

    没有开启新线程

    串行执行任务

    没有开启新线程

    串行执行任务

    没有开启新线程

    串行执行任务

    异步(async)

    开启一个新线程

    串行执行任务

    开启多个新线程

    并行执行任务

    没有开启新线程

    串行执行任务

    3.7.6死锁

    死锁是同步(sync)线程操作中比较常见的一个问题,要理解死锁,我们要深刻的理解这么一句话:串行与并行说的是队列,队列是用来调度和存放任务的;而同步与异步,针对的是线程。区别在于,同步会阻塞当前线程,必须要等待同步线程中的任务执行完成以后,才能继续执行下一任务;而异步线程则是不用等待。下面我们来分析几个示例,更细的理解死锁是怎么来的,又是怎么没的。

    示例1:

    1 NSLog(@"任务1 thread:%@", [NSThread currentThread]); //任务1 2 dispatch_sync(dispatch_get_main_queue(), ^{ 3 NSLog(@"任务2 thread:%@", [NSThread currentThread]); //任务2 4 }); 5 NSLog(@"任务3 thread:%@", [NSThread currentThread]); // 任务3

    结果:

    发现后面的任务2和任务3都没有打印。

    3.8dispatch_once

    从函数名就知道,这个函数用于保证在应用程序执行中只执行一次指定处理。通常用于实现单例。

    1 + (HDFNetConfig *)sharedConfig { 2 static id sharedInstance = nil; 3 static dispatch_once_t onceToken; 4 dispatch_once(&onceToken, ^{ 5 sharedInstance = [[self alloc] init]; 6 }); 7 return sharedInstance; 8 }

     

    转载请注明原文地址: https://ju.6miu.com/read-37458.html

    最新回复(0)