[面试时]我是如何讲清楚GCD的

时间:2022-06-25 05:18:40

1、概述

GCD是苹果引入的多线程编程技术方案,原名“Grand Central Dispatch”,在GCD之前,已经出现了NSThread、NSOperationQueue,两项多线程解决方案,其对比如下:

- GCD NSOperatinQueue NSThread
解析 基于C语言框架,充分利用多核,苹果推荐的多线程技术 面向对象多线程编程 轻量级多线程编程,真正的多线程编程技术,每一个NSThread对象对应一个线程
抽象程度
优点 基于C语言,是替代前两者的高效强大的技术,执行效率比前两者高,代码比较集中易维护 不用担心线程管理、同步的事情,主要精力在对线程的操作上,是面向对象的,建立在GCD上,在架构上、优先级管理上做的比GCD好,可以很好支持GCD没有或者不完美的操作,比如取消线程、KVO监控、指定操作间的优先级或相互依赖 量级轻,使用简单
缺点 - 相对GCD来说代码比较分散,不容易管理 需要自己管理线程的生命周期、线程同步、睡眠、唤醒等,对线程加锁需要耗费一定系统开销

2、多线程编程

如下图,一般一个CPU(单核)只能执行一个线程(相当于图中一条不分叉的路径),通过切换目标路径专用的内存块,复原CPU寄存器等信息,切换到另外一条路径(即上下文切换)的方式实现并发执行,多核CPU则是多个线程并行执行。

[面试时]我是如何讲清楚GCD的

应用程序在启动时,通过最先执行的线程,即“主线程”来描绘用户界面,处理触摸屏幕的事件等。如果在该主线程中进行长时间的处理,如AR用画像的识别或数据库访问,就会妨碍主线程的执行(造成阻塞)。如下图,因此使用多线程编程,在执行长时间的处理时仍可保证用户界面的响应性能。

[面试时]我是如何讲清楚GCD的

3、GCD的API

3.1 Dispatch Queue

Dispatch Queue顾名思义就是执行处理的等待队列,它分为两种:Serial Dispatch Queue 和Concurrent Dispatch Queue,其对比如下:

- Serial Dispatch Queue Concurrent Dispatch Queue
特点 等待现在执行中处理的结束,即线程队列非并发 不等待现在执行中处理的结束,即线程队列并发
区别 同时执行的线程数只有1个 同时执行的线程数多个,具体取决于系统状态
用途 线程执行顺序要求固定时使用 期望多个线程并发执行时使用

3.2 dispatch_queue_create

虽然Serial Dispatch Queue、Concurrent Dispatch Queue受到系统资源的限制,但是用dispatch_queue_create可以生成任意多个Dispatch Queue,因此你就可以生成任意多个Serial Dispatch Queue并行执行,但是酱紫容易导致资源竞争、死锁等线程同步问题。

dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue",NULL);

/*
第一个参数指定Serial Dispatch Queue名称,推荐使用应用程序ID这种逆序全域名,第二个参数指定是Serial Dispatch Queue还是Concurrent Dispatch Queue,默认NULL是前者
*/

因此当你想并行执行线程有不发生资源竞争时,建议使用Concurrent Dispatch Queue。由于Concurrent Dispatch Queue生成的线程由XNU内核管理,因此不会发生数据竞争等问题。

dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue",DISPATCH_QUEUE_CONCURRENT);

尽管有ARC技术,但是creat生成的Dispatch Queue还是要程序员自己释放,因为ARC不会应用到GCD上

dispatch_release(mySerialDispatchQueue);

dispatch_retain(mySerialDispatchQueue);

3.3 Main Dispatch Queue / Global Dispatch Queue

Main Dispatch Queue 中的线程会在主线程中的RunLoop中执行,也就等于Serial Dispatch Queue,主要用来处理用户界面的界面更新等线程。

dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue(); 

Global Dispatch Queue是所有程序都能够使用的Concurrent Dispatch Queue。有四个优先级,分别是DISPATCH_QUEUE_PRIORITY_HIGH(高优先级)、DISPATCH_QUEUE_PRIORITY_DEFAUT(默认优先级)、DISPATCH_QUEUE_PRIORITY_LOW(低优先级)、DISPATCH_QUEUE_PRIORITY_BACKGROUND(后台运行)

dispatch_queue_t globalDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); 

Main Dispatch Queue 、Global Dispatch Queue是另外两种生成线程队列的方法,前者生成的队列在主线程中处理,后者让程序员更加方便的使用Concurrent Dispatch Queue。另外和普通creat出来的线程队列不同的是,dispatch_retain和dispatch_release对二者不会引起任何变化。

3.4 dispatch_sync / dispatch_async

dispatch_async意味着“非同步”,即将指定的Block“非同步”的追加到指定的Dispatch Queue中,dispatch_async函数不用做任何等待。

dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.example.gcd.MyConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(myConcurrentDispatchQueue, ^{NSLog(@"block on myConcurrentDispatchQueue");});
dispatch_release(myConcurrentDispatchQueue);

dispatch_sync意味着“同步”,即将指定的Block“同步”的追加到指定的Dispatch Queue中,dispatch_sync函数会一直等待。用法:比如执行Main Dispatch Queue时,使用另外的吸纳成Global Dispatch Queue进行处理,处理结束后立即使用所得到的结果。代码如下:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{/*处理*/});

但是dispatch_sync容易产生死锁问题,因此要谨慎使用。如下,假如Main Dispatch Queue正在处理Block,此时Block又在等待其执行结束并追加到Main Dispatch Queue中,酱紫就造成了死锁

dispatch_queue_t queue = dispatch_get_main_queue();
disptch_sync(queue, ^{NSLog(@"Hello?");});

3.5 dispatch_set_target_queue

用dispatch_queue_create函数生成的Dispatch Queue都是默认优先级的线程,要改变线程的优先级,dispatch_set_target_queue的作用就凸显出来了,栗子如下:

dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);

不仅如此,该函数还可以改变执行层次,最底层的Serial Dispatch Queue和Concurrent Dispatch Queue在按照顺序在上一层的Serial Dispatch Queue中执行,避免最底层线程并行执行。

[面试时]我是如何讲清楚GCD的

3.6 dispatch_after

想在3秒后执行线程处理,实现方式如下:

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{NSLog(@"waited at least three seconds.");});

3.7 Dispatch Group

实现追加到Dispatch Queue中的多个处理全部结束后想执行结束处理

dispatch_queue_t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, queue, ^{NSLog(@"blk0");});
dispatch_group_async(group, queue, ^{NSLog(@"blk1");});
dispatch_group_async(group, queue, ^{NSLog(@"blk2");});
dispatch_group_async(group, queue, ^{NSLog(@"blk3");});

dispatch_group_notify(group, dispatch_get_main_queue(); ^{NSLog(@"done");});

// dispatch_group_wait(group, DISPATCH_TIME_FOREVER); 
//仅等待全部处理执行结束

//dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
//long result = dispatch_group_wait(group, time);
//if(result == 0)
//{/*Dispatch Group的线程全部执行结束*/}
//else
//{/*至少有一个Dispatch Group线程还在执行*/}

dispatch_release(group);

Tip:
1、一般情况下推荐用dispatch_group_notify函数追加结束处理到Main Dispatch Queue中。
2、Dispatch Group底层实现能够够根据系统资源状况来决定给某队列配备多个线程,也会在适当的时机自动创建新线程或复用旧线程。这种简便的方式,既可以并发执行一系列给定任务,又可以在全部任务结束时得到通知。开发者仅仅需要关注业务逻辑代码,无序自己编写调度器。

3.8 dispatch_barrier_async

在访问数据库或文件时,读取操作可以并行执行,但是如果在其中插入写操作,那就要保证写操作时只有唯一线程在访问数据库或文件,这时候就可以用dispatch_barrier_async来实现。当然在Serial Dispatch Queue中就不用担心这个问题。如此实现可以提高访问数据库的效率。

dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.ForBarrier", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);

dispatch_barrier_async(queue, blk_for_writing);

dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);

执行效果如下图:

[面试时]我是如何讲清楚GCD的

3.9 dispatch_apply

dispatch_apply函数是dispatch_sync函数和Dispatch Group的关联API,该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等待全部处理执行结束。可以实现对数组array[]的遍历。

dispatch\_queue\_t queue = dispatch\_get\_global\_queue(DISPATCH\_QUEUE\_PRIORITY\_DEFAULT, 0);
dispatch_apply([array count], queue, ^(size_t index){
    NSLog(@"%zu: %@", index, [array objectAtIndex:index]); 
});

3.10 dispatch_suspend / dispatch_resume

dispatch_suspend挂起指定的Dispatch Queue

dispatch_suspend(queue);

dispatch_resume恢复指定的Dispatch Queue

dispatch_resume(queueu);

3.11 Dispatch Semaphore

其实相当于多线程中的信号量,规定多线程中允许多少个线程并发执行。计数为0时,等待,计数为1或大于1时,减去1而不等待。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_semaphore_t semaphore = dispatch_semapore_create(1);

NSMutableArray *array = [[NSMutableArray alloc] init]; for(int i = 0; i < 10000; ++i) { dispatch_async(queue, ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); [array addObject:[NSNumber numberWithInt:i]];

        dispatch_semaphore_signal(semaphore);
    });
}
dispatch_release(semaphore);

3.12 dipatch_once

保证在应用程序执行中执行一次指定处理,可以用来创建单例

static dispatch_once_t pred;
dispatch_once(&pred, ^{/*处理*/});

3.13 Dispatch I/O

能够提高文件读取的速度。

3.14 Dispatch Source

Dispatch Queue 没有“取消”的概念,但是Dispatch Source是可以取消的,而且取消时必须执行处理可指定为回调的Block形式。

3.15 dispatch_get_current_queue

最好不要使用dispatch_get_current_queue,实际上,从iOS6.0起,该函数已经正式弃用了。原因是函数行为常常和开发者所预期的不同。可以用“队列特定值”方法来代替。

performSelector与GCD

在GCD之前,Objective-C的动态性之一performSelector发挥着重要作用,但是现在最好避免使用,特别是在ARC环境下,原因如下:

  • performSelector系列方法在内存管理方面容易疏失。因为无法确定执行的选择子是什么,因此ARC编译器无法适当插入内存管理方法
  • performSelector能够处理的选择子太过于局限,包括选择子参数和返回值
  • GCD能够完美的代替performSelector