Objective-C高级编程读书笔记之GCD

Objective-C高级编程 iOS与OS X多线程和内存管理

Objective-C高级编程读书笔记三部曲已经写完, 另外两篇如下 :
Objective-C高级编程读书笔记之内存管理
Objective-C高级编程读书笔记之blocks


Grand Central Dispatch (GCD)

目录

  1. 什么是GCD
  2. 什么是多线程, 并发
  3. GCD的优势
  4. GCD的API介绍
  5. GCD的注意点
  6. GCD的使用场景
  7. Dispatch Source
  8. 总结

1. 什么是GCD

GCD, Grand Central Dispatch, 可译为"强大的中枢调度器", 基于libdispatch, 纯C语言, 里面包含了许多多线程相关非常强大的函数. 程序员可以既不写一句线程管理的代码又能很好地使用多线程执行任务.

GCD中有Dispatch QueueDispatch Source. Dispatch Queue是主要的, 而Dispatch Source比较次要. 所以这里主要介绍Dispatch Queue, 而Dispatch Source在下面会简单介绍.

Dispatch Queue

苹果官方对GCD的说明如下 :

开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中.

这句话用源代码表示如下

dispatch_async(queue, ^{
    /*
     * 想执行的任务
     */  
});

该源码用block的语法定义想执行的任务然后通过dispatch_async函数讲任务追加到赋值在变量queue的"Dispatch Queue"中.

Dispatch Queue究竟是什么???

Dispatch Queue是执行处理的等待队列, 按照先进先出(FIFO, First-In-First-Out)的顺序进行任务处理.

First-In-First-Out

另外, 队列分两种, 一种是串行队列(Serial Dispatch Queue), 一种是并行队列(Concurrent Dispatch Queue).

Dispatch Queue的种类 说明
Serial Dispatch Queue 等待现在执行中处理结束
Concurrent Dispatch Queue 不等待现在执行中处理结束
串行队列

串行队列 : 让任务一个接一个执行

并行队列

并发队列 : 让多个任务同时执行(自动开启多个线程执行任务)
并发功能只有在异步函数(dispatch_async)下才有效(想想看为什么?)

GCD的API会在下面详细说明~


2. 什么是多线程, 并发

我们知道, 一个应用就相当于一个进程, 而一个进程可以同时分发几个线程同时处理任务.而并发正是一个进程开启多个线程同时执行任务的意思, 主线程专门用来刷新UI,处理触摸事件等 而子线程呢, 则用来执行耗时的操作, 例如访问数据库, 下载数据等..

以前我们CPU还是单核的时候, 并不存在真正的线程并行, 因为我们只有一个核, 一次只能处理一个任务. 所以当时我们计算机是通过分时也就是CPU地在各个进程之间快速切换, 给人一种能同时处理多任务的错觉来实现的, 而现在多核CPU计算机则能真真正正货真价实地办到同时处理多个任务.


3. GCD的优势

说到优势, 当然有比较, 才能显得出优势所在. 事实上, iOS中我们能使用的多线程管理技术有

  • pthread
  • NSThread
  • GCD
  • NSOperationQueue

pthread

来自Clang, 纯C语言, 需要手动创建线程, 销毁线程, 手动进行线程管理. 而且代码极其恶心, 我保证你写一次不想写第二次...不好意思我先去吐会T~T

NSThread :

Foundation框架下的OC对象, 依旧需要自己进行线程管理,线程同步。 线程同步对数据的加锁会有一定的开销。

GCD :

两个字, 牛逼, 虽然是纯C语言, 但是它用难以置信的非常简洁的方式实现了极其复杂的多线程编程, 而且还支持block内联形式进行制定任务. 简洁! 高效! 而且我们再也不用手动进行线程管理了.

NSOperationQueue :

相当于Foundation框架的GCD, 以面向对象的语法对GCD进行了封装. 效率一样高.

GCD优势在哪里?

  1. GCD会自动利用更多的CPU内核
  2. GCD会自动管理线程的生命周期
  3. 使用方法及其简单

怎么样? 心动不, 迫不及待想要知道怎么使用GCD了吧, 那我们马上切入正题~


4. GCD的API介绍

在介绍GCD的API之前, 我们先搞清楚四个名词: 串行, 并行, 同步, 异步

  • 串行 : 一个任务执行完, 再执行下一个任务
  • 并行 : 多个任务同时执行
  • 同步 : 在当前线程中执行任务, 不具备开启线程的能力
  • 异步 : 在新的线程中执行任务, 具备开启线程的能力
串行, 并行, 同步, 异步的关系

下面开始介绍GCD的API

创建队列

dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)

手动创建一个队列.

  • label : 队列的标识符, 日后可用来调试程序
  • attr : 队列类型
    DISPATCH_QUEUE_CONCURRENT : 并发队列
    DISPATCH_QUEUE_SERIAL 或 NULL : 串行队列

需要注意的是, 通过dispatch_queue_create函数生成的queue在使用结束后需要通过dispatch_release函数来释放.(只有在MRC下才需要释放)

并不是什么时候都需要手动创建队列, 事实上系统给我们提供2个很常用的队列.

主队列

dispatch_get_main_queue();

该方法返回的是主线程中执行的同步队列. 用户界面的更新等一些必须在主线程中执行的操作追加到此队列中.

全局并发队列

dispatch_get_global_queue(long identifier, unsigned long flags);

该方法返回的是全局并发队列. 使用十分广泛.

  • identifier : 优先级
    DISPATCH_QUEUE_PRIORITY_HIGH : 高优先级
    DISPATCH_QUEUE_PRIORITY_DEFAULT : 默认优先级
    DISPATCH_QUEUE_PRIORITY_LOW : 低优先级
    DISPATCH_QUEUE_PRIORITY_BACKGROUND : 后台优先级
  • flags : 暂时用不上, 传 0 即可

注意 : 对Main Dispatch Queue和Global Dispatch Queue执行dispatch_release和dispatch_retain没有任何问题. (MRC)

同步函数

dispatch_sync(dispatch_queue_t queue, ^(void)block);

在参数queue队列下同步执行block

异步函数

dispatch_async(dispatch_queue_t queue, ^(void)block);

在参数queue队列下异步执行block(开启新线程)

时间

dispatch_time(dispatch_time_t when, int64_t delta);

根据传入的时间(when)和延迟(delta)计算出一个未来的时间

  • when :
    DISPATCH_TIME_NOW : 现在
    DISPATCH_TIME_FOREVER : 永远(别传这个参数, 否则该时间很大)
  • delta : 该参数接收的是纳秒, 可以用一个宏NSEC_PER_SEC来进行转换, 例如你要延迟3秒, 则为 3 * NSEC_PER_SEC.

延迟执行

dispatch_after(dispatch_time_t when, dispatch_queue_t queue, ^(void)block);

有了上述获取时间的函数, 则可以直接把时间传入, 然后定义该延迟执行的block在哪一个queue队列中执行.

苹果还给我们提供了一个在主队列中延迟执行的代码块, 如下

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            code to be executed after a specified delay
        });

我们只需要传入需要延迟的秒数(delayInSeconds)和执行的任务block就可以直接调用了, 方便吧~

注意 : 延迟执行不是在指定时间后执行任务处理, 而是在指定时间后将处理追加到队列中, 这个是要分清楚的

队列组

dispatch_group_create();

有时候我们想要在队列中的多个任务都处理完毕之后做一些事情, 就能用到这个Group. 同队列一样, Group在使用完毕也是需要dispatch_release掉的(MRC). 上代码

group

组异步函数

dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, ^(void)block);

分发Group内的并发异步函数

组通知

dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, ^(void)block)

监听group的任务进度, 当group内的任务全部完成, 则在queue队列中执行block.

组等待

dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)

  • timeout : 等待的时间
    DISPATCH_TIME_NOW : 现在
    DISPATCH_TIME_FOREVER : 永远

该函数会一直等待组内的异步函数任务全部执行完毕才会返回. 所以该函数会卡住当前线程. 若参数timeout为DISPATCH_TIME_FOREVER, 则只要group内的任务尚未执行结束, 就会一直等待, 中途不能取消.

栅栏

dispatch_barrier_async(dispatch_queue_t queue, ^(void)block)

在访问数据库或文件时, 为了提高效率, 读取操作放在并行队列中执行. 但是写入操作必须在串行队列中执行(避免资源抢夺问题). 为了避免麻烦, 此时dispatch_barrier_async函数作用就出来了, 在这函数里进行写入操作, 写入操作会等到所有读取操作完毕后, 形成一道栅栏, 然后进行写入操作, 写入完毕后再把栅栏移除, 同时开放读取操作. 如图

dispatch_barrier_async

快速迭代

dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index){ 
    // code here
});

执行10次代码, index顺序不确定. dispatch_apply会等待全部处理执行结束才会返回. 意味着dispatch_apply会阻塞当前线程. 所以dispatch_apply一般用于异步函数的block中.

一次性代码

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ 
// 只执行1次的代码(这里面默认是线程安全的)
});

该代码在整个程序的生命周期中只会执行一次.

挂起和恢复

dispatch_suspend(queue)

挂起指定的queue队列, 对已经执行的没有影响, 追加到队列中尚未执行的停止执行.

dispatch_resume(queue)

恢复指定的queue队列, 使尚未执行的处理继续执行.

5. GCD的注意点

因为在ARC下, 不需要我们释放自己创建的队列, 所以GCD的注意点就剩下死锁

死锁
NSLog(@"111");
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"222");
});
NSLog(@"333");

以上三行代码将输出什么?
111
222
333 ?
还是
111
333 ?
其实都不对, 输出结果是
111

为什么? 看下图

死锁

毫无疑问会先输出111, 然后在当前队列下调用dispatch_sync函数, dispatch_sync函数会把block追加到当前队列上, 然后等待block调用完毕该函数才会返回, 不巧的是, block在队列的尾端, 而队列正在执行的是dispatch_sync函数. 现在的情况是, block不执行完毕, dispatch_sync函数就不能返回, dispatch_sync不返回, 就没机会执行block函数. 这种你等我, 我也等你的情况就是死锁, 后果就是大家都执行不了, 当前线程卡死在这里.

如何避免死锁?

不要在当前队列串行队列中使用同步函数, 在队列嵌套的情况下也不允许. 如下图,

队列嵌套调用同步函数引发死锁

大家可以想象, 队列1执行完NSLog后到队列2中执行NSLog, 队列2执行完后又跳回队列1中执行NSLog, 由于都是同步函数, 所以最内层的NSLog("333"); 追加到队列1中, 实际上最外层的dispatch_sync是还没返回的, 所以它没有执行的机会. 也形成死锁. 运行程序, 果不其然, 打印如下 :
111
222

6. GCD的使用场景

线程间的通信

这是GCD最常用的使用场景了, 如下代码

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 执行耗时操作
            
    dispatch_async(dispatch_get_main_queue(), ^{
        // 回到主线程作刷新UI等操作
    });
});

为了不阻塞主线程, 我们总是在后台线程中发送网络请求, 处理数据, 然后再回到主线程中刷新UI界面

单例

单例也就是在程序的整个生命周期中, 该类有且仅有一个实例对象, 此时为了保证只有一个实例对象, 我们这里用到了dispatch_once函数

static XXTool *_instance;

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [self allocWithZone:zone];
    });
    
    return _instance;
}

+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] init];
    });
    
    return _instance;
}

- (id)copy
{
    return _instance;
}

- (id)mutableCopy
{
    return _instance;
}

因为alloc内部会调用allWithZone, 所以我们重写allocWithZone方法就行了. 通过以上代码可以保证程序只能创建一个实例对象, 并且该实例对象永远存在程序中.

同步队列和锁

我们知道, 属性中有atomic和nonatomic属性

  • atomic : setter方法线程安全, 需要消耗大量的资源
  • nonatomic : setter方法非线程安全, 适合内存小的移动设备

为了实现属性线程安全, 避免资源抢夺的问题, 我们也许会这样写

- (NSString *)setMyString:(NSString *)myString
{
    @synchronized(self) {
        _myString = myString;
    }
}

这种方法没错是可以达到该属性线程安全的需求, 但是试想一下, 如果一个对象中有许多个属性都需要保证线程安全, 那么就会在self对象上频繁加锁, 那么两个毫无关系的setter方法就有可能执行一个setter方法需要等待另一个setter方法执行完毕解锁之后才能执行, 这样做毫无必要. 那么你有可能会说, 在每个方法内部创建一个锁对象就好啦, 不过你不觉得这样会浪费资源吗?

那么能不能利用队列, 实现getter方法可以并发执行, 而setter方法串行执行并且setter和getter不能并发执行呢??? 没错, 我们这里用到了dispatch_barrier_async函数.

- (NSString *)myString
{
    __block NSString *localMyString = nil;
    
    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        localMyString = self.myString;
    });
    
    return localMyString;
}

- (void)setMyString:(NSString *)myString
{
    dispatch_barrier_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        _myString = myString;
    });
}

这里利用了栅栏块必须单独执行, 不能与其他块并行的特性, 写入操作就必须等当前的读取操作都执行完毕, 然后单独执行写入操作, 等待写入操作执行完毕后再继续处理读取.

7. Dispatch Source

它是BSD系内核惯有功能kqueue的包装. kqueue的CPU负荷非常小, 可以说是应用程序处理XNU内核中发生的各种事件的方法中最优秀的一种.

但是由于Dispatch Source实在是太少人用了, 所以这里不再介绍. 感兴趣的朋友们可以自行Google.

8. 总结

  • GCD可进行线程间通信
  • GCD可以办到线程安全
  • GCD可用于延迟执行
  • GCD需要注意死锁问题(不要在当前队列调用同步函数)

想再往深了解并发编程, 可以看看这篇文章
并发编程 : API及挑战


欢迎大家关注@Jerry4me, 关注菜鸟成长_. 我会不定时更新一些学习心得与文章.

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

推荐阅读更多精彩内容