iOS多线程系列之三:GCD用法大全

一、GCD简介

GCD(Grand Central Dispatch) 伟大的中央调度系统,是苹果为多核并行运算提出的C语言并发技术框架。

GCD会自动利用更多的CPU内核;
会自动管理线程的生命周期(创建线程,调度任务,销毁线程等);
程序员只需要告诉 GCD 想要如何执行什么任务,不需要编写任何线程管理代码

一些专业术语

dispatch :派遣/调度
    
queue:队列
    用来存放任务的先进先出(FIFO)的容器
sync:同步
    只是在当前线程中执行任务,不具备开启新线程的能力
async:异步
    可以在新的线程中执行任务,具备开启新线程的能力
concurrent:并发
    多个任务并发(同时)执行
串行:
    一个任务执行完毕后,再执行下一个任务

二、GCD中的核心概念

1.任务

任务就是要在线程中执行的操作。我们需要将要执行的代码用block封装好,然后将任务添加到队列并指定任务的执行方式,等待CPU从队列中取出任务放到对应的线程中执行。

 - queue:队列
 - block:任务
// 1.用同步的方式执行任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

// 2.用异步的方式执行任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

// 3.GCD中还有个用来执行任务的函数
// 在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

2.队列

队列以先进先出按照执行方式(并发/串行)调度任务在对应的线程上执行;
队列分为:自定义队列、主队列和全局队列;

<1>自定义队列
自定义队列又分为:串行队列和并发队列

  • 串行队列
    串行队列一次只调度一个任务,一个任务完成后再调度下一个任务
// 1.使用dispatch_queue_create函数创建串行队列
////OC
// 创建串行队列(队列类型传递NULL或者DISPATCH_QUEUE_SERIAL)
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", NULL);
////Swift
let serialQueue = DispatchQueue(label: "serialQueue")

// 2.获得主队列
////OC
dispatch_queue_t mainQueue = dispatch_get_main_queue();
////Swift
let mainQueue = DispatchQueue.main
注意:主队列是GCD自带的一种特殊的串行队列,放在主队列中的任务,都会放到主线程中执行。
  • 并发队列
    并发队列可以同时调度多个任务,调度任务的方式,取决于执行任务的函数;并发功能只有在异步的(dispatch_async)函数下才有效;异步状态下,开启的线程上限由GCD底层决定。
// 1.使用dispatch_queue_create函数创建队列
dispatch_queue_t
dispatch_queue_create(const char *label, // 队列名称,该名称可以协助开发调试以及崩溃分析报告 
dispatch_queue_attr_t attr); // 队列的类型

// 2.创建并发队列
////OC
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
////Swift
let concurrentQueue = DispatchQueue(label: "concurrentQueue",attributes:.concurrent)

自定义队列在MRC开发时需要使用dispatch_release释放队列

#if !__has_feature(objc_arc)
    dispatch_release(queue);
#endif

<2>主队列
主队列负责在主线程上调度任务,如果在主线程上有任务执行,会等待主线程空闲后再调度任务执行。
主队列用于UI以及触摸事件等的操作,我们在进行线程间通信,通常是返回主线程更新UI的时候使用到

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 耗时操作
    // ...
    //放回主线程的函数
    dispatch_async(dispatch_get_main_queue(), ^{
        // 在主线程更新 UI
    });
});

<3>全局并发队列

全局并发队列是由苹果API提供的,方便程序员使用多线程。

//使用dispatch_get_global_queue函数获得全局的并发队列
dispatch_queue_t dispatch_get_global_queue(dispatch_queue_priority_t priority, unsigned long flags);
// dispatch_queue_priority_t priority(队列的优先级 )
// unsigned long flags( 此参数暂时无用,用0即可 )

//获得全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

全局并发队列有优先级

//全局并发队列的优先级
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高优先级
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认(中)优先级
//注意,自定义队列的优先级都是默认优先级
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低优先级
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台优先级

然而,iOS8 开始使用 QOS(quality of service
) 替代了原有的优先级。获取全局并发队列时,直接传递 0,可以实现 iOS 7 & iOS 8 later 的适配。

//像这样
dispatch_get_global_queue(0, 0);

<4>全局并发队列与并发队列的区别

全局并发队列与并发队列的调度方法相同
全局并发队列没有队列名称
在MRC开发中,全局并发队列不需要手动释放

<5>QOS (服务质量) iOS 8.0 推出

QOS_CLASS_USER_INTERACTIVE:用户交互,会要求 CPU 尽可能地调度此任务,耗时操作不应该使用此服务质量
QOS_CLASS_USER_INITIATED:用户发起,比 QOS_CLASS_USER_INTERACTIVE 的调度级别低,但是比默认级别高;耗时操作同样不应该使用此服务质量;如果用户希望任务尽快执行完毕返回结果,可以选择此服务质量;
QOS_CLASS_DEFAULT:默认,此 QOS 不是为添加任务准备的,主要用于传送或恢复由系统提供的 QOS 数值时使用
QOS_CLASS_UTILITY:实用,耗时操作可以使用此服务质量;
QOS_CLASS_BACKGROUND:后台,指定任务以最节能的方式运行
QOS_CLASS_UNSPECIFIED:没有指定 QOS

3.执行任务的函数

<1>同步(dispatch_sync)

执行完这一句代码,再执行后续的代码就是同步

任务被添加到队列后,会当前线程被调度;队列中的任务同步执行完成后,才会调度后续任务。-在主线程中,向主队列添加同步任务,会造成死锁
-在其他线程中,向主队列向主队列添加同步任务,则会在主线程中同步执行。
具体是否会造成死锁,以及死锁的原因,还需要针对具体的情况分析,理解队列和执行任务的函数才是关键。实际开发中一般只要记住常用的组合就可以了。
我们可以利用同步的机制,建立任务之间的依赖关系
例如:

用户登录后,才能够并发下载多部小说
只有“用户登录”任务执行完成之后,多个下载小说的任务才能够“异步”执行
所有下载任务都依赖“用户登录”

<2>异步(dispatch_async)

不必等待这一句代码执行完,就执行下一句代码就是异步

异步是多线程的代名词,当任务被添加到主队列后,会等待主线程空闲时才会调度该任务;添加到其他线程时,会开启新的线程调度任务。
<3>以函数指针的方式调度任务
函数指针的调用方式有两种,同样是同步和异步;函数指针的传递类似于 pthread。

dispatch_sync_f
dispatch_async_f

函数指针调用在实际开发中几乎不用,只是有些面试中会问到,dispatch + block 才是 gcd 的主流!

4.开发中如何选择队列

选择队列当然是要先了解队列的特点
串行队列:对执行效率要求不高,对执行顺序要求高,性能消耗小
并发队列:对执行效率要求高,对执行顺序要求不高,性能消耗大
如果不想兼顾 MRC 中队列的释放,建议选择使用全局队列 + 异步任务。

三、GCD的其他用法

1.延时执行

参数1:从现在开始经过多少纳秒,参数2:调度任务的队列,参数3:异步执行的任务
dispatch_after(when, queue, block)
例如:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 2秒后异步执行这里的代码...
});

2.一次性执行

应用场景:保证某段代码在程序运行过程中只被执行一次,在单例设计模式中被广泛使用。

// 使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只执行1次的代码(这里面默认是线程安全的)
});

3.调度组(队列组)

应用场景:需要在多个耗时操作执行完毕之后,再统一做后续处理

//创建调度组
dispatch_group_t group = dispatch_group_create();
//将调度组添加到队列,执行 block 任务
dispatch_group_async(group, queue, block);
//当调度组中的所有任务执行结束后,获得通知,统一做后续操作
dispatch_group_notify(group, dispatch_get_main_queue(), block);

例如:

// 分别异步执行2个耗时的操作、2个异步操作都执行完毕后,再回到主线程执行操作
dispatch_group_t group =  dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 执行1个耗时的异步操作
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 执行1个耗时的异步操作
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 等前面的异步操作都执行完毕后,回到主线程...
});

4.定时器

//创建代码
dispatch_source_t CreateDispatchTimer(uint64_t interval,
  uint64_t leeway,
  dispatch_queue_t queue,
  dispatch_block_t block)
{
 dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
       0, 0, queue);
 if (timer)
 {
 dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
 dispatch_source_set_event_handler(timer, block);
 dispatch_resume(timer);
 }
 return timer;
}
  • Dispatch Source Timer 是间隔定时器,也就是说每隔一段时间间隔定时器就会触发。在 NSTimer 中要做到同样的效果需要手动把 repeats 设置为 YES。

  • dispatch_source_set_timer 中第二个参数,当我们使用dispatch_time 或者 DISPATCH_TIME_NOW 时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用 dispatch_walltime 可以让计时器按照真实时间间隔进行计时。

dispatch_time与dispatch_walltime 区别
使用第一个函数创建的是一个相对的时间,第一个参数开始时间参考的是当前系统的时钟,当 device 进入休眠之后,系统的时钟也会进入休眠状态, 第一个函数同样被挂起; 假如 device 在第一个函数开始执行后10分钟进入了休眠状态,那么这个函数同时也会停止执行,当你再次唤醒 device 之后,该函数同时被唤醒,但是事件的触发就变成了从唤醒 device 的时刻开始,1小时之后
而第二个函数则不同,他创建的是一个绝对的时间点,一旦创建就表示从这个时间点开始,1小时之后触发事件,假如 device 休眠了10分钟,当再次唤醒 device 的时候,计算时间间隔的时间起点还是 开始时就设置的那个时间点, 而不会受到 device 是否进入休眠影响

  • dispatch_source_set_timer 的第四个参数 leeway 指的是一个期望的容忍时间,将它设置为 1 秒,意味着系统有可能在定时器时间到达的前 1 秒或者后 1 秒才真正触发定时器。在调用时推荐设置一个合理的 leeway 值。需要注意,就算指定 leeway 值为 0,系统也无法保证完全精确的触发时间,只是会尽可能满足这个需求。

  • event handler block 中的代码会在指定的 queue 中执行。当 queue 是后台线程的时候,dispatch timer 相比 NSTimer 就好操作一些了。因为 NSTimer 是需要 Runloop 支持的,如果要在后台 dispatch queue 中使用,则需要手动添加 Runloop。使用 dispatch timer 就简单很多了。

  • dispatch_source_set_event_handler 这个函数在执行完之后,block 会立马执行一遍,后面隔一定时间间隔再执行一次。而 NSTimer 第一次执行是到计时器触发之后。这也是和 NSTimer 之间的一个显著区别。
    停止 Timer

停止 Dispatch Timer 有两种方法,一种是使用 dispatch_suspend,另外一种是使用 dispatch_source_cancel。

dispatch_suspend 严格上只是把 Timer 暂时挂起,它和 dispatch_resume 是一个平衡调用,两者分别会减少和增加 dispatch 对象的挂起计数。当这个计数大于 0 的时候,Timer 就会执行。在挂起期间,产生的事件会积累起来,等到 resume 的时候会融合为一个事件发送。
注意
dispatch_source_cancel 则是真正意义上的取消 Timer。被取消之后如果想再次执行 Timer,只能重新创建新的 Timer。这个过程类似于对 NSTimer 执行 invalidate。

关于取消 Timer,另外一个很重要的注意事项:dispatch_suspend 之后的 Timer,是不能被释放的!因此使用 dispatch_suspend 时,Timer 本身的实例需要一直保持。使用 dispatch_source_cancel 则没有这个限制。

下面的代码会引起崩溃:
- (void)stopTimer
{
 dispatch_suspend(_timer);//EXC_BAD_INSTRUCTION 崩溃
 //dispatch_source_cancel(_timer);//OK
 _timer = nil; // 
}

四、基于GCD的单例模式

作用:
可以保证在程序运行过程,一个类只有一个实例,而且该实例易于供外界访问。从而方便地控制了实例个数,并节约系统资源
使用场合:
在整个应用程序中,共享一份资源(这份资源只需要创建初始化1次)

实现方法
重写实现

// 1.在.m中保留一个全局的static的实例
static id _instance;

// 2.重写allocWithZone:方法,在这里创建唯一的实例(注意线程安全)
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [super allocWithZone:zone];
    });
    return _instance;
}

// 3.提供1个类方法让外界访问唯一的实例
+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] init];
    });
    return _instance;
}

// 4.实现copyWithZone:方法
- (id)copyWithZone:(struct _NSZone *)zone
{
    return _instance;
}

宏实现

// .h文件
#define SingletonH(name) + (instancetype)shared##name;

// .m文件
#define SingletonM(name) 
static id _instance; 
 
+ (instancetype)allocWithZone:(struct _NSZone *)zone 
{ 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        _instance = [super allocWithZone:zone]; 
    }); 
    return _instance; 
} 
 
+ (instancetype)shared##name 
{ 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        _instance = [[self alloc] init]; 
    }); 
    return _instance; 
} 
 
- (id)copyWithZone:(NSZone *)zone 
{ 
    return _instance; 
}

五、如何取消GCD任务

有一部分人说GCD无法取消任务,也有人站出反对说话不负责任。那么我们先来看看他提供的方案:return就可以正常结束一段代码

- (void)viewDidLoad {
    [super viewDidLoad];

    [self gcdTest];
}


- (void)gcdTest{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 模拟耗时操作
        for (long i=0; i<100000; i++) {
            NSLog(@"i:%ld",i);
            sleep(1);
            // 山不过来,我就过去
            if (gcdFlag==YES) {
                NSLog(@"收到gcd停止信号");
                return ;
            }
        };
    });

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"发出停止gcd信号!");
        gcdFlag = YES;
    });
}

GCD中的定时器

//0.创建一个队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    //1.创建一个GCD的定时器
    /*
     第一个参数:说明这是一个定时器
     第四个参数:GCD的回调任务添加到那个队列中执行,如果是主队列则在主线程执行
     */
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

    //2.设置定时器的开始时间,间隔时间以及精准度

    //设置开始时间,三秒钟之后调用
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW,3.0 *NSEC_PER_SEC);
    //设置定时器工作的间隔时间
    uint64_t intevel = 1.0 * NSEC_PER_SEC;

    /*
     第一个参数:要给哪个定时器设置
     第二个参数:定时器的开始时间DISPATCH_TIME_NOW表示从当前开始
     第三个参数:定时器调用方法的间隔时间
     第四个参数:定时器的精准度,如果传0则表示采用最精准的方式计算,如果传大于0的数值,则表示该定时切换i可以接收该值范围内的误差,通常传0
     该参数的意义:可以适当的提高程序的性能
     注意点:GCD定时器中的时间以纳秒为单位(面试)
     */

    dispatch_source_set_timer(timer, start, intevel, 0 * NSEC_PER_SEC);

    //3.设置定时器开启后回调的方法
    /*
     第一个参数:要给哪个定时器设置
     第二个参数:回调block
     */
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"------%@",[NSThread currentThread]);
    });

    //4.执行定时器
    dispatch_resume(timer);

    //注意:dispatch_source_t本质上是OC类,在这里是个局部变量,需要强引用
    self.timer = timer;

GCD定时器补充
/*
 DISPATCH_SOURCE_TYPE_TIMER         定时响应(定时器事件)
 DISPATCH_SOURCE_TYPE_SIGNAL        接收到UNIX信号时响应

 DISPATCH_SOURCE_TYPE_READ          IO操作,如对文件的操作、socket操作的读响应
 DISPATCH_SOURCE_TYPE_WRITE         IO操作,如对文件的操作、socket操作的写响应
 DISPATCH_SOURCE_TYPE_VNODE         文件状态监听,文件被删除、移动、重命名
 DISPATCH_SOURCE_TYPE_PROC          进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号

 下面两个都属于Mach相关事件响应
    DISPATCH_SOURCE_TYPE_MACH_SEND
    DISPATCH_SOURCE_TYPE_MACH_RECV
 下面两个都属于自定义的事件,并且也是有自己来触发
    DISPATCH_SOURCE_TYPE_DATA_ADD
    DISPATCH_SOURCE_TYPE_DATA_OR
 */

iOS多线程系列之一:多线程基础
iOS多线程系列之二: NSThread
iOS多线程系列之三:GCD
iOS多线程系列之四:NSOperation以及多线程技术比较

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容